# 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. 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 absolute path an agent prints. - **In the response viewer**, where paths are usually written as prose or in backticks. They render as underlined monospace links. 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. 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-