Files
Codeman/docs/wiki/Working-With-Files.md
T
RandalixandClaude Opus 5.5 0a6a877415 fix(edit-history): remote reads get time for their size, follow CLAUDE_CONFIG_DIR, keep the conversation after /clear
Review findings on the remote edit history:

- A forward read may carry 13 MiB and "Load older" 29 MiB; the flat 30 s
  read timeout cut those off below ~1 MiB/s. The timeout now adds the
  transfer time at 256 KiB/s (remoteEditSourcesTimeoutMs). It ends a hung
  read, it does not cut a slow one.
- The transcript lookup looked only under $HOME/.claude. It now uses an
  absolute CLAUDE_CONFIG_DIR of the ssh command's environment, else
  $HOME/.claude, for both the projects and the live sessions/<pid>.json.
  A value set only in .zshrc stays invisible (the lookup is a plain sh);
  documented, and the panel's remote no-transcript note says so instead
  of promising the next prompt (a remote host sends no hooks).
- After a remote /clear claudeSessionId stays the launch id, so once the
  remote claude exited the lookup fell back to the cleared conversation
  and the panel flipped back. The sync now hands the lookup the stored
  conversation, tried right after the live one (remoteTranscriptIds).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-05 11:19:52 +01:00

362 lines
24 KiB
Markdown

# Working With Files
Reading, editing, attaching, and previewing files without leaving the dashboard. Useful on
a desktop; on a phone it is the difference between reviewing an agent's work and waiting
until you get home.
## The File Viewer
A panel that browses the active session's working directory. Its header button is on by
default; if it is missing, re-enable it in **App Settings → Header & Panels**.
It renders what it can:
| Kind | Behaviour |
| ------------------------ | ------------------------------------------------------------------------- |
| Text and code | Plain preview with Lines (line numbers) and Wrap toggles in the header. Long files are truncated in plain preview. |
| Markdown | Rendered by default: headings, tables, code blocks with copy buttons, images and links relative to the file (root-relative ones resolve from the workspace root, as on GitHub). Links to another heading of the same file (`[Install](#installation)`) scroll to it, with GitHub's heading names (lower-case, punctuation dropped, repeats numbered `-1`, `-2`), and never leave the page. Opened from an attachment card, where the file's folder is unknown, relative images show their alt text and relative links show as plain text. The MD pill in the header flips to source. |
| Images | Inline. |
| Audio and video | Inline with a working scrub bar, because range requests are supported. |
| Spreadsheets (`.xlsx`) | Read-only grid, parsed in your browser (never on the server), up to 10 MB. `.xls` and `.ods` are download only. |
| PDF and Office documents | Converted for preview when a converter is available. |
| Anything else | Download. |
Caps: 10 MB for text preview, 2 GB for raw and download (set `CODEMAN_MAX_DOWNLOAD_BYTES`
to change it, `0` for no limit — these bodies are streamed, so a large file costs a read
stream rather than server memory). Sensitive paths (`.env`, anything
matching credentials, `~/.ssh`, AWS credentials) are blocked from download, and SVG and HTML
are served as downloads rather than rendered, so they cannot execute in the page.
The preview window has **Back** and **Forward** buttons (also `Alt+←` and `Alt+→`) once you
have opened a second file, so a link into another document is no longer one way. **Expand**
fills the screen and is remembered per device, and the corner grip resizes the window. On a
phone the window is always full screen, so Expand and the grip are hidden there.
Closing the preview pauses and unloads any playing media. A video that keeps playing after
you close the panel means you are on an old version.
## Editing in place
Text files can be edited and saved directly in the viewer. Click the pencil in the preview
header, edit, **Save**.
The guardrails are worth knowing, because they are what makes editing safe rather than
convenient:
- **Extension allowlist**, not a blocklist. Code, docs, config, and markup are editable.
Anything not on the list is not.
- **512 KB cap** on both read and write.
- **Edit mode never truncates.** The plain preview does truncate long files, and saving a
truncated buffer would silently delete the rest, so the editor loads the whole file or
refuses.
- **Optimistic concurrency.** The save carries a hash of what you started from. If the file
changed underneath you (likely, when an agent is working in the same repo), the save is
rejected rather than clobbering their work.
- **No file creation.** Writes go to a temporary file and are renamed over the original, and
the open never creates. Editing in place is structural, not a rule.
- **Line endings are preserved** server-side, so editing two lines of a CRLF file does not
produce a whole-file diff.
- **`.git/` is denied outright.** Hooks are executable code, and a corrupted index looks
unrecoverable to someone who wanted to fix a typo.
- **Non-UTF-8 content is refused**, verified by a round-trip comparison.
## Attachments
Attachments are live references to files **outside** the session's workspace: a spec on your
desktop, a PDF in Downloads, a design document elsewhere on the machine.
Register one from the CLI:
```bash
codeman attach /path/to/spec.pdf
```
An attachment card appears in the session, and the file can be previewed inline. The
attachment gets a stable id, and browser requests use that id rather than carrying absolute
paths around.
Agents can register attachments too, by emitting a `codeman://attach?...` link in their
output. That path is **prompt-injectable by nature**, so it is force-confined to the
session's workspace: a hostile prompt cannot use it to pull arbitrary host files into the
event stream. The gate is an extension allowlist rather than a blocklist.
Document conversion for previews is globally rate limited. Without that, ten large documents
detected at once would fork ten multi-minute converter processes.
## Clicking a path
File paths in a session are links. That works in two places:
- **In the terminal**, on any path an agent or a command prints. A relative path (`src/app.ts`,
`../notes.md`) resolves against the directory the session's pane is in now, so after a `cd`
in a shell it opens the file there; until the pane reports a directory, and always for remote
SSH and Docker cases, it resolves against the session's working directory.
- **In the response viewer**, where paths are usually written as prose or in backticks. They
render as underlined monospace links. A Markdown link whose target is a path
(`[report](/tmp/run/report.json)`, the way Codex cites files) opens the same way.
Clicking one opens it in the preview: images and PDFs render, video and audio play with a
working scrub bar, documents convert, text shows inline, Markdown renders and HTML opens as a
page. The exception is a plain text file (a log, `.txt`, `.json`, code) inside the workspace
clicked in the terminal: that opens in the tail viewer instead, which follows a file that is
still being written. A Markdown file always opens rendered, wherever you click it.
Paths **outside** the session's workspace work too, which matters because that is where most
of an agent's output lands: a screenshot in `/tmp`, a capture in its own scratchpad, a file in
another checkout. Those are served through the attachment routes rather than the workspace
ones, so the same rules apply as to any other attachment: secret trees are blocked, the
extension allowlist decides what can be opened, and symlinks are resolved before either check.
Outside the workspace the allowlist is images, video, audio, PDF, Office documents, and text
files, where "text" is the same list the viewer will let you edit: code, config, logs, csv,
markdown. The reasoning is that a session can already `cat` any of those, so the file suffix
was never what kept anything secret; the path guard is. Types outside the list (`.svg`,
`.bmp`) say so rather than failing silently.
An `.html` file opens as a rendered page by default, with its relative CSS, scripts, images and
data files. The page is served from a separate `/html-view/...` address in a sandbox with no
origin of its own, so its scripts can never read Codeman's page, cookies or API.
By the project owner's decision, a rendered page has the same network access as a file you open
from disk in your browser. It **can** load scripts, styles, fonts, images and media from
anywhere on the internet (a dashboard that pulls Chart.js from a CDN works), send requests to
any host, and read the files in its own folder. It **cannot** act as Codeman: it has no access
to Codeman's page, cookies or API, and no access to any folder other than its own. Since a page
can send what it can read to any host, open HTML only from sources you trust, as you would on
your own desktop. The **Page**
pill in the viewer header switches to the source view (where **Edit** works as for any text
file), and the viewer falls back to source on its own when a page cannot be rendered: a remote
(SSH) case, an HTML file inside a hidden folder, or one sitting directly in a broad folder such
as your home folder or `/tmp` (the page may read the files in its own folder tree, so move it
into a folder of its own). The raw file routes still serve HTML as a download only.
Text previews are capped at the first 500 lines, fetched as a partial read, so clicking a
one-gigabyte log does not try to paint one.
Log-shaped files inside the workspace still open in the tail viewer, which follows a file as
it is written. Outside the workspace they open in the preview instead: the tail viewer runs
`tail -f`, and that is deliberately restricted to the workspace, `/var/log` and `~/logs`.
Nothing is registered until you click. Opening a file this way does not add an attachment card.
## Remote (SSH) cases
In a remote case the workspace lives on the other machine, and so do the files. Previews,
downloads, text reads and the clicked-path route all go over the same ssh connection the
session uses: one `realpath` plus `stat` probe for the file and the workspace root, then a
streamed `cat` (or a slice of it, so video seeking works). Symlinks are resolved on the host
that can resolve them, the size cap applies to the remote size before a byte is requested,
and an unreachable host answers 502 rather than pretending the file is missing. Nothing is
ever copied onto the Codeman host, and a same-named local file is never served under a
remote name.
Not available over ssh, and said so with a 400 instead of a misleading 404: editing in
place, Office previews and generated thumbnails (both need the bytes on the server's disk),
the file tree and path picker, and the tail viewer. Docker cases are unaffected, because
their workspace is bind-mounted at the same path.
## The path picker
For choosing a path rather than typing one. It appears in two places:
- **Browse** in **Add Case → Link Existing**.
- The **📁 Path** key on the mobile keyboard bar.
It browses one directory at a time and can show hidden entries on request. The current
folder is an editable field: type or paste a path and press Enter (or **Go**) to jump
straight there, and a full file path lands in its folder with that file selected. The
**Sort** control orders each listing by name or by modified time (newest first is the
quick way to the file an agent just wrote), with folders always ahead of files; the
choice is remembered per device. The picker inserts the path into your prompt
**without** pressing Enter, so nothing is submitted by accident. Its sibling **⌫ All**
key clears the unsent prompt, and never sends the agent's `/clear` command.
In **Link Existing**'s picker, **+📁** (New folder) makes a folder inside the folder you
are viewing and selects it. Names use letters, numbers, spaces and `_ - . ~` (not starting
with a dot or `~`). The cases folder is refused, since every folder there is already a
case: use **Add Case → New Case** instead.
This is a separate file-serving surface from the viewer, with its own rules: it allowlists
your home directory, the cases directory, and anything in `CODEMAN_FILE_PICKER_ROOTS`, and
blocks sensitive trees. In multi-user mode a non-admin gets only their own user space as a
root, because per-user spaces live inside the home directory and a home-directory root would
expose everyone.
## Files into a session
Paste from the clipboard or drag and drop straight onto the terminal. The file is written
where the agent can read it and the reference is inserted into your prompt. On a phone, the
toolbar's Attach button opens the camera, photo library or files, and puts the path on your
prompt, where it behaves like text you typed; the Attach button inside the Compose editor
does the same into the editor. Dropping a video or a document onto the terminal works the
same way.
Images (png, jpg, gif, webp, bmp, heic) are re-encoded on the device before they go up (a GIF
goes as is, so it keeps its frames), and HEIC from an iPhone is converted to JPEG. Videos (mp4, webm, mov, m4v, ogv) are stored as recorded,
up to 1 GB each by default (`CODEMAN_MAX_PASTE_VIDEO_BYTES`); they stream to disk rather than
through server memory. Documents are the office formats (pdf, docx, pptx, xlsx, zip) and the
text family (txt, md, csv, tsv, json, log, yaml, yml, xml). Each is checked on the way in,
against its file signature for the binary formats and against its encoding for text (UTF-8,
no NUL bytes), so a script renamed to `.pdf` is refused while a `.txt` can hold any text at
all. A text file in another encoding is refused with a message saying it is not UTF-8 (or that
it is UTF-16, which Excel's "Unicode Text" and Notepad's "Unicode" produce); save it as UTF-8
(in Excel, "CSV UTF-8") and attach it again. Files with script or page extensions (sh, py,
js, html) are not accepted, since a prompt upload is something for the agent to read, not
something for Codeman to store and serve. A document keeps a trimmed copy of its original
name, letters, digits, dot, dash and underscore with anything else turned into a dash, so
`Q3 report.pdf` is findable as `paste-<time>-<id>-Q3-report.pdf`; a name made entirely of
other characters saves without one. Uploads are refused for remote (SSH) sessions, whose
working directory is on the other host.
Uploads land in `.codeman-uploads/` inside the case, next to the code the agent is working
on, because that is the one path a local agent and a Docker case (whose workspace is mounted
at the same absolute path) both see the same way. The first upload writes a `.gitignore` of `*` into `.codeman-uploads/`, so the
directory ignores itself and your repository never picks the files up, while your own ignore file is
left alone; a `.gitignore` you already had there is never overwritten. Files older than seven
days are swept hourly, and the directory goes when the last session of the case is closed (a
session that is only detached keeps it). Earlier releases wrote to `.claude-images/`; that
directory is still swept and cleaned up the same way for one release so a case in use before
the move is not left with stray files.
## Generated artifacts
When an agent produces a file the UI can show (a chart, a diagram, a document), it can
surface as an artifact attachment rather than a path you have to go and find.
## Git changes
Agents often leave work uncommitted or unpushed. Turn on **App Settings → Header & Panels →
Bottom bar → Git status** (per device, off by default) and the right of the bottom bar shows
the active session's repository: `● 3` uncommitted files, `↑ 2` commits not pushed, `⚠` merge
conflicts, `? 1` a repository git could not read, `✓` when everything is committed and pushed.
Click it for a draggable window, in the style of the File Viewer:
- **Uncommitted changes**, grouped as staged, not staged, untracked and conflicted, each with a
status letter (`M` modified, `A` added, `D` deleted, `R` renamed, `?` new, `U` conflict).
- **Not pushed**: the commits no remote has. A branch with no upstream says so, and so does one whose
upstream does not exist on the remote, because it was never pushed or was deleted there ("Upstream
not on remote"), which counts every commit on no remote rather than showing a green tick.
- Files are grouped under their folders, collapsed until you click a folder (a chain of single-child
folders is one row, and the folders you opened stay open when the list refreshes). Turn off
**App Settings → Header & Panels → Bottom bar → Git status: group files by folder** for a flat
list of full paths instead.
- **Click a file** to see what changed in it, as a unified diff with added and removed lines
coloured. Staged files show index versus last commit, not-staged files show working tree
versus index, untracked files show as all additions and deleted files as all removals.
**Open file** jumps to the File Viewer; **Back** returns to the list. A binary file shows a
note instead, and a diff over 400 KB is cut short.
- A session folder that holds several projects gets one collapsible section per repository
found up to two levels down (up to **Git status: max repositories**, 12 by default; the window says
when there are more). A repository git could not read, typically a timeout on a slow network
share, is listed with the reason and counted as `? N` in the bottom-bar indicator, never silently
left out; the **git timeout** setting raises how long it waits. They all start collapsed (each summary line shows its branch and
what is outstanding), and the ones you open stay open when the window refreshes; an unrelated repository above the workspace (a dotfiles repo
in your home folder) is ignored.
- **Fetch** in the header runs `git fetch` for the repositories the window lists, on your click; the
window then re-reads, so "behind" is as of that fetch. A repository that could not be fetched keeps
its row and shows why. **Git fetch on an interval** (Settings → Bottom bar, off by default) runs the
same fetch on a timer for every session that is on, and **Session Options → Git** overrides both the
switch and the interval for one session: a session that turned itself on runs even while the app-wide
switch is off, sessions open on the same folder share one run, and nothing is ever fetched for Docker or
remote (SSH) sessions.
It is read-only apart from that fetch: Codeman never commits, checks out or changes the
repository, and `git fetch` writes remote-tracking refs only; it is the only git command the Git window
runs that writes, the only one that can run a repository's own hooks (`reference-transaction`,
`pre-auto-gc` through the maintenance a fetch triggers, and config commands such as `credential.helper`),
and the only one that can run later by itself, on a timer: cloning and worktree management write too, but
only when you asked for them at the moment you asked, which is why nothing runs on a timer until you opt in. A fetch holds git's ref lock, so an agent's own `git pull` in the same repository
can hit the same `cannot lock ref`, and a folder is fetched by one run at a time. It is not shown for Docker
or remote (SSH) sessions, and a repository at or inside a Docker case
workspace is skipped even from a local session, before any git runs (a container can write there,
and git would run that repository's own configuration on the host). Fetch asks nothing of you: it
uses the sign-in your own git already has, and it gives up quickly rather than waiting on an
invisible login prompt. That BatchMode ssh is a default, never an override: your own
`GIT_SSH_COMMAND`, or the repository's `core.sshCommand`, is used when either is set, and ⚠️ the default's
`StrictHostKeyChecking=accept-new` records an unknown host key in `~/.ssh/known_hosts` without asking:
BatchMode stops a prompt, not a write. The
data comes from `GET /api/sessions/:id/git-status`, `GET /api/sessions/:id/git-diff` and, on that
click or the interval you turned on, `POST /api/sessions/:id/git-fetch`
(see the [API reference](https://github.com/Ark0N/Codeman/blob/master/docs/api-reference.md)).
## Edit history
The Git panel shows what is different now. The edit history shows what the agent did, one edit
at a time, including edits that were later undone or committed. Turn on **App Settings → Header &
Panels → Header → Edit history** (per device, off by default) and a clock button appears in the
header next to Attachments. It is not shown on phones.
Click it for a draggable window listing the active session's edits, newest first:
- One row per Edit or Write, with `new` for a file the agent created, `edit` otherwise,
and the lines added and removed. **Click a row** to see that edit's diff inline, coloured like the
Git panel; a diff over 64 KB is cut short. **Click the path** to open the file in the preview.
- Edits made by subagents carry a chip with the subagent's task; hover it for the agent type and
its worktree branch. **Main agent only** hides them. **Group by file** puts the edits under
their file, with the totals on the file's row.
- Images an agent produced with a shell command (a render, a plot, a screenshot) appear as rows with
a thumbnail: a `.png`, `.jpg`, `.gif`, `.webp`, `.avif` or `.svg` path named in the command or
its output counts when the file's modification time falls within that command's run. Images the
agent sent you count as well. Images outside the workspace and SVGs show no thumbnail. Click one
to open it in the preview.
- **Load older** reads further back in the conversation, in the main agent's transcript and the
subagents' alike (up to three transcripts per click; with more, they take turns).
- After `/clear` or a resume, the edits of the earlier conversation stay listed, and the window
first catches up on the ones it had not read yet. **Load older** then goes back within the new
conversation.
It works for Claude sessions, local and remote (SSH). Docker and other CLIs' sessions say so instead.
A remote session's images show no thumbnail (each would be one more SSH read); click one to
preview it. A remote host that is asleep is never woken for the history: the window shows the
stored edits with the SSH error on top.
How it reads:
- **Only while you look.** The history is read from the session's Claude transcript when you open
the window, and again after each tool call that can change files and at the end of each turn,
while it stays open on that session (a remote session at the end of each turn only: its host
sends no tool events). A closed window reads nothing, and Codeman does no work in the
background for sessions nobody looks at.
- **The transcript is found through the session's hooks.** Each prompt tells Codeman where the
session's transcript is. Right after a server restart, before the next prompt, Codeman looks in
the session's `CLAUDE_CONFIG_DIR` if you set one for it in Codeman, else in the default config
directory. A `CLAUDE_CONFIG_DIR` set outside Codeman (in a shell profile or a wrapper script)
is only known after the next prompt; until then the window says "No transcript found for this
session yet". A remote session's transcript is looked up on its host over SSH: the
conversation its Claude writes now, else the one the window showed last, else the session's
own, under the remote user's `~/.claude`, or under `CLAUDE_CONFIG_DIR` when the host sets it
for every SSH command (`.zshenv`, `/etc/environment`); one set only in `.zshrc` is not seen.
- **The history is stored, and it holds secrets.** Every diff an agent wrote is copied to
`~/.codeman/edit-history/<session-id>.json`, including API keys or `.env` contents it edited. The file is
readable by your user only, capped at 500 edits and 4 MiB of diff text per session (the oldest
are dropped), never part of search, blocked from every file route, and deleted when you delete
the session (a file left behind, by a crash for instance, goes at the next start). A pinned session you kill keeps its record and its history until you unpin and
delete it.
- **Images go through the existing file routes.** A thumbnail inside the workspace uses the File
Viewer's route. An image outside it opens like any clicked path outside the workspace (no
notification, same file-type allowlist and sensitive-file blocklist). No new route serves a file.
The data comes from `GET /api/sessions/:id/edits`, `GET /api/sessions/:id/edits/:entryId` and
`POST /api/sessions/:id/edits/older` (see the
[API reference](https://github.com/Ark0N/Codeman/blob/master/docs/api-reference.md)).
## Gotchas
- **The viewer follows the active session's workspace.** Switching tabs changes what you are
browsing.
- **A save can be rejected, and that is the feature.** It means the agent edited the file
while you were typing. Re-open, re-apply, save again.
- **Attachments live outside the workspace on purpose.** For files inside it, just use the
viewer.
- **`.env` files are readable in the viewer if the extension policy allows the preview, but
never downloadable.** Do not treat the viewer as a secrets boundary; treat the machine as
the boundary.
## Read next
- [The Dashboard](The-Dashboard) - where the panels live.
- [Input And Voice](Input-And-Voice) - other ways to get content into a session.
- [Security](Security) - how the file surfaces are confined.
- [`docs/file-viewer-edit-plan.md`](https://github.com/Ark0N/Codeman/blob/master/docs/file-viewer-edit-plan.md) - the edit-mode design.