-
Notifications
You must be signed in to change notification settings - Fork 0
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.
/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.
-
/pasteis macOS only: it reads a PNG off the mac clipboard (pngpastewhen installed, otherwise the built-inosascript). Elsewhere, save the image and/sendit.
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 offNone 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.
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
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).
/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.
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.
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
-
/filesonly knows what a tool call announced (anEdit/Write/Readfile_path). A file changed by a shell command inside aBashcall is invisible to it — that is what/diffis for. -
/diffisgit diff: the unstaged working tree only, capped at 120 lines. If the host's--cwdis not in a git repository it says so. - Both go through the same best-effort secret mask as every other outbound row.
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.