Repository navigation
Prompt uploads: where they should land, documents and video, and a visible Attach on phones #553
Description
On #529 you said you'd follow up separately on where uploads should land (a visible temp/ versus a hidden Codeman folder like .claude-images/). I've been working on the same area from the prompt side and ran into that question too, so here's a proposal for it plus two PRs that are built and tested but that I'm holding until you've weighed in on the location.
Where uploads land
Today paste-image writes into <workspace>/.claude-images/. That name works for Claude, but nine of the ten run modes aren't Claude, and once documents come in it isn't only images either.
The rule I'd suggest: a file goes where the one who has to open it can reach it.
- Things the agent opens go in the workspace, because that's the only path that resolves the same for a local agent, a Docker case (only the workspace is bind-mounted, at the same absolute path) and a remote host.
- Things only Codeman reads stay in
~/.codeman. - Another tool's config stays in that tool's directory.
For uploads that gives <workspace>/.codeman/attachments/, with a .codeman/.gitignore containing *. That keeps uploads out of the user's repo without touching their own ignore file. The ignore file is written once, with exclusive create, so a user's own file there is never overwritten. The old .claude-images/ is never written again but stays readable for one release, so the GC sweep and the delete cleanup still cover it. Both directory levels are checked to be real directories, so a planted .codeman -> /other-case/.codeman symlink can't carry the recursive delete or the sweep into another tree.
A visible temp/ is the other option. My worry with it is that it lands in git status and in the agent's own view of the repo. If you prefer it, the PRs only need the directory constant changed: the names live in one place (paste-image-gc.ts).
Other Codeman artifacts could move under .codeman/ later (Ralph's @fix_plan.md and @ralph_prompt.md at the repo root, .claude/ralph-loop.local.md), but that changes the prompt contract the agent follows, so I've kept it out of these PRs.
What the two PRs do
1. A visible Attach button on phones, plus video. Today the only way to hand an agent a photo from a phone is to raise the keyboard, open Compose and tap its Image key, so with the keyboard down there's nothing to tap.
- The phone toolbar gets an Attach button that opens the native picker (photos, camera, files). The picked path goes onto the prompt as unsent text, so you can add words and send.
- Videos are accepted through
?kind=videoonpaste-image. They are streamed to disk under their own cap (1 GB,CODEMAN_MAX_PASTE_VIDEO_BYTES) and the container is checked from the first bytes as they arrive (mov, mp4, m4v, webm, 3gp, mkv). The image path is byte-identical without the flag, so an image can never ride the larger cap through memory. - One client classifier (
_promptAttachKind()) now feeds every pick and paste filter, instead of four scatteredtype.startsWith('image/')checks. - A remote (SSH) session is refused before anything touches disk, because its
workingDiris a path on the remote host.
Tested in a real iOS Safari simulator and on my iPhone. One iOS 26.x quirk is WebKit's, not ours: the keyboard doesn't come back by itself after the picker closes, so it takes one tap on the prompt. iOS 27 restores it on its own.
2. Documents.
paste-imagealso admits pdf, docx, pptx, xlsx, zip and the text family (txt, md, csv, tsv, json, log, yaml, yml, xml), recognized by the file name's extension.- Each upload is then checked on its bytes by the same fail-closed sniffer:
%PDF-, the zip signature for the OOXML trio and zip, and UTF-8 with no NUL byte for text. That is a signature and encoding check, not a content filter. - Script and markup types (sh, py, js, html) stay refused by extension.
- Saved names keep a sanitized stem, for example
paste-<ts>-<hex>-My_Report.pdf. - Dispatch is MIME-first, so anything the image path accepted before is still accepted.
- This PR moves uploads to
.codeman/attachments/.
How this relates to #529
#529 adds an upload from the Files panel into temp/. That's a different entry point (browsing a workspace) from Attach (adding to the prompt you're typing), so I don't think they compete. If you go with .codeman/attachments/, both could write to the same place. @shenlvkang-collab, I didn't want this to land on your PR as a surprise.
Questions
.codeman/attachments/, a visibletemp/, or something else?- Is the document list right? Anything you'd add or drop?
- Would you rather review these as two PRs, as above, or differently?
I'll open the PRs once the location is settled, rebased onto master. Both have been running on my own install since 2026-10-07.
Activity
Ark0N commentedon Oct 9, 2026
Thanks for writing this up before opening the PRs, @JDProfresh, it made the decision easy. Short version: yes to a hidden folder that Codeman owns and that ignores itself, but under a different name than
.codeman/.1. Location:
<workspace>/.codeman-uploads/I agree with your rule (a file goes where whoever opens it can reach it) and with what follows from it: the workspace rather than
~/.codeman, hidden rather thantemp/(a visibletemp/shows up ingit status, gets committed by accident and sits in the agent's view of the repo), the.gitignorecontaining*written once with exclusive create, the real-directory checks, and.claude-images/staying readable for one release.The one thing I'd change is the name. When the workspace is the home directory,
<workspace>/.codeman/IS Codeman's data dir: that dir isjoin(homedir(), '.codeman')(src/config/instance.ts:66), and nothing refuses home as a workspace (safePathSchemaonly asks for an absolute path, soPOST /api/cases/linkandPOST /api/sessionsboth accept/home/<you>). A session there would writeattachments/and a.gitignoreinto the directory that holdsstate.json,users.jsonand the app clone. Your version survives that, because the recursive delete only targetsattachments/, but your own roadmap moves more Codeman files under.codeman/later, and then the workspace folder and the data dir are one tree.A flat
.codeman-uploads/avoids that, follows the.codeman-*prefix a case root already carries (.codeman-agent-case.json), and leaves one directory level to check for symlinks instead of two. Please still have the sweep and the delete cleanup refuse a resolved directory at or undergetDataDir().CODEMAN_INSTANCE=uploadswould make the names meet again, which is contrived, but it's one check next to a recursive delete.I picked "uploads" over "attachments" because Codeman already has an attachments subsystem that goes the other way (
attachment-registry.ts, the/attachmentsroutes,codeman://attach: files the agent hands to the user). Keeping the two words apart keeps the code and the docs readable. The button can still say Attach.Ralph's files staying where they are for now: agreed, and the shared prefix leaves room for them later.
2. The document list
Fine as proposed. A few notes:
file-rawserves non-media text as inerttext/plainwithnosniff. Please word it that way in the code. html/htm/svg are the ones that really have to stay out.VIDEO_ATTACHMENT_EXTENSIONSinattachment-registry.ts(mp4, webm, mov, m4v, ogv). It's single-sourced so that what a user hands in also plays in the file viewer. 3gp and mkv would upload but not preview; if you've seen phones produce them, adding them is fine, just say so in the PR.session-routes.ts:5134) means up to 30 GB of disk a minute, which is no longer the bound that limit was written for. A separate, much lower rate for?kind=video, or a per-workspace budget, would close that. Please also add a test that the image path's cap is unchanged without the flag.FILE_PATH_LINK_PATTERN(constants.js), so those paths won't be clickable when the agent echoes them.3. PR split: your two, plus a small one in front
Please move the relocation out into its own PR and land it first. It's small, but it touches the recursive delete, and otherwise the Attach/video PR ships writing into
.claude-images/and has to move again. It should cover every site that names the old dir:session-routes.ts:5246),paste-image-gc.tsand the cleanup inserver.ts:1538, includingpasteImageDirInUseByOtherSession().claude-images/staying covered by both the sweep and the delete cleanup for one releasedocs/architecture-invariants.md(two passages name.claude-images) and the comments inreboot-restore-routes.tsandterminal-ui.jstest/paste-image-dir-shared.test.ts, the path regex intest/routes/session-routes.test.ts, andtest/link-provider-regex.test.tsOne more for that PR: the workspace file watcher (
image-watcher.ts, off by default) excludes nothing upload-related today. With it on, a user's own upload (pdf and docx are detected types) comes back as a "detected" card, so itsignoredfilter should skip the upload dir.Then Attach + video and documents on top, in either order.
paste-imageis part of the public API, so please describe the new behaviour indocs/api-reference.mdas well.#529
@shenlvkang-collab, this settles what I left open on your PR. I'll leave a note there too.
JDProfresh commentedon Oct 9, 2026
Thanks, that settles it.
.codeman-uploads/it is, and the data-dir check sits next to the delete.The relocation is #574, on its own as you asked, covering the sites you listed plus the image watcher's ignore filter and the port-0 conversion of
test/paste-image-dir-shared.test.ts. Attach + video and documents follow on top of it once it lands, reshaped to your notes: the video list starts fromVIDEO_ATTACHMENT_EXTENSIONS(dropping 3gp and mkv, since I never saw a phone produce either; those test files were made with ffmpeg),?kind=videogets its own much lower rate, a test pins the image cap without the flag, the sh/py/js refusal is worded as a scope choice, and both describepaste-imageindocs/api-reference.md.feat(uploads): write prompt uploads to a hidden, self-ignoring .codem…
Ark0N commentedon Oct 14, 2026
Status, for anyone landing here. Two of the three parts have shipped:
<workspace>/.codeman-uploads/(feat(uploads): write prompt uploads to a hidden, self-ignoring .codeman-uploads/ folder #574) in 1.41.0.Documents are #621, and the client-side filter for video containers the route refuses is #622. Both are open. I'll leave this issue open until #621 lands. @JDProfresh, if you add
Closes #553to #621's description, GitHub closes it on merge.Ark0N commentedon Oct 16, 2026
All three parts of this plan have shipped, so I'm closing it. Thanks again @JDProfresh for writing it up so clearly and then building all of it:
<workspace>/.codeman-uploads/, hidden and self-ignoring (feat(uploads): write prompt uploads to a hidden, self-ignoring .codeman-uploads/ folder #574, 1.41.0).The follow-ups from the #621 review (a clearer refusal for non-UTF-8 CSV/TXT exports, a parity test for the document list) and the partial-refusal toast you offered on #622 are welcome as their own PRs.