Skip to content

Files and Export

Roy Padina edited this page Aug 30, 2026 · 4 revisions

Files and Export

Three directions: a guest sends the host a file, the host offers one back, and a guest takes the whole transcript home.

Everything a client writes lands in its own working directory, mode 0644, never executable, and nothing here ever runs or opens what it just wrote.

Guest → host: /send and /paste

/send ~/Downloads/spec.pdf
/paste                          # the clipboard as a PNG
/paste the failing screen       # …with a caption

The host is asked (/accept-file or /deny-file, or one key on the approval bar). On approval the file lands in <host-cwd>/jam-uploads/ and claude is told to look at it.

  • Cap: 20 MB. Over that it is refused before any bytes move.
  • The name is sanitized on both sides — a name with a path in it is refused, and a collision gets a numbered suffix rather than overwriting.
  • One transfer in flight per person; a second while the first is still going is refused.
  • /paste is macOS only: it reads a PNG off the mac clipboard (pngpaste when installed, otherwise the built-in osascript). Elsewhere, save the image and /send it.

Whether the host is asked at all (--uploads)

A jam where three people are pasting screenshots is a jam where the host does nothing but press a. So the host chooses once, at launch or from /menu → Access → Uploads:

policy what happens
ask (default) every transfer goes to the host, exactly as above
auto anyone already admitted — knock-approved, token or invite link — may send with no prompt. Still announced to everyone, still logged: ⇪ Yossi sent screenshot.png (2.1 MB) → jam-uploads/…. Just not a question
off every upload refused with a reason, including the host's own /paste, and a standing always grant does not override it
claude-jam host --uploads auto
claude-jam host --uploads auto --upload-quota 500MB
claude-jam host --uploads off

None of the real protections move with the policy. Under auto exactly as under ask: the basename is sanitized and a traversal name is refused, the 20 MB cap holds, one transfer at a time per person, writes go only into <cwd>/jam-uploads/, nothing is executed or opened, and an announced-vs-actual byte mismatch drops the upload. Since 0.22.1 the write is exclusive, so a symlink planted in that directory under the incoming name is refused rather than followed, and the session quota counts grants in flight as well as bytes that have landed, so several people sending at once cannot overshoot it. The policy decides only whether the host is asked.

"Sanitized" can mean renamed, and it is worth knowing which renames happen: anything outside [A-Za-z0-9._-] becomes _, a leading dot goes (no dotfiles), a name over 80 characters is cut but keeps its extension, trailing dots go, and a Windows device name — con, prn, aux, nul, com1…, lpt1…, with or without an extension — gets an underscore in front, so con.txt lands as _con.txt. Those names are not files on Windows: a write to nul silently discards and con is the console. The rename happens on every platform, so a jam never hands the room a name that works on the host and not on somebody's machine. scripts/smoke-nudge.mjs proves each of those still refuses while the policy is auto — that is the point of the smoke.

The quota auto needs. An auto session may take 40 files or 200 MB, whichever comes first. After that the policy falls back to ask and says so once — upload quota reached — asking again — so an auto jam cannot quietly fill a disk. --upload-quota <n>[MB|files] changes it; /menu → Access → Upload quota shows what has been spent and resets it. If somebody asks "why did it ask me this time when it didn't last time", this is the answer.

Host → guest: an offer, then /get

The host runs /send <path> too, but for them it offers the file rather than uploading it. Every guest sees the offer; each one takes it with

/get              # the newest offer
/get spec.pdf     # a named one

which writes it into that guest's own ./jam-downloads/.

  • Cap: 50 MB for an offer.
  • Nobody is made to take anything: an offer is a name and a size until somebody asks for it.

/export — the transcript

/export

A guest asks; the host approves (/allow-export / /deny-export). The whole session transcript arrives as ./jam-session-<session-id>.jsonl in the guest's own directory, capped at 50 MB.

Export has its own toggle, and it stays ask. --export ask|auto|off at launch, and /menu → Access → Export the transcript at runtime. It does not follow --uploads: a jam whose uploads are auto still asks before the transcript leaves, and there is no quota on it. The defaults differ on purpose — a file is one file, and a transcript is the whole conversation (see What an export actually contains below).

Resuming it on your own machine

/export prints the recipe with your real values filled in. It is:

mkdir -p ~/.claude/projects/<slug>
cp jam-session-<id>.jsonl ~/.claude/projects/<slug>/<id>.jsonl
claude --resume <id>

<slug> is the host's working directory with every non-alphanumeric character turned into -. A different cwd means a different folder, so use the slug the export printed rather than inventing one — ls ~/.claude/projects to check what is there.

What an export actually contains

Everything claude saw in that session: file contents it read, tool output, the whole context. claude-jam strips its own join-token block and the raw token from the copy, and nothing else. That is a courtesy, not a scrub.

Run /token new after an export. See Security Model.

/files and /diff

Not transfers, but the other half of "what has this session touched":

/files            # every path a tool call read, wrote or edited — newest first, with counts
/diff             # git diff --stat of the host's working tree
/diff src/app.ts  # the real hunks for one path
  • /files only knows what a tool call announced (an Edit/Write/Read file_path). A file changed by a shell command inside a Bash call is invisible to it — that is what /diff is for.
  • /diff is git diff: the unstaged working tree only, capped at 120 lines. If the host's --cwd is not in a git repository it says so.
  • Both go through the same best-effort secret mask as every other outbound row.

The approval ladder these share

Uploads, offers, exports, guest commands and permission answers all use one ladder: default deny, one request in flight per person, a two-minute expiry, and always for standing approval that lives in daemon memory and dies with the daemon. Every refusal is sent to that person's own client as a ! line with the actual reason.

Next

Security Model · Joining a Jam · Troubleshooting

Clone this wiki locally