-
Notifications
You must be signed in to change notification settings - Fork 0
Security Model
Read this before you put a jam on anything but a LAN you own.
The short version: admission is real, and once somebody is in, everybody is equally trusted. Some of the guards below are enforced by code and some are only instructions to a language model. This page says which is which, because planning around the wrong one is how people get hurt.
These are enforced in code, have tests, and a refusal carries its own reason.
-
Token — 8–64 characters of
[A-Za-z0-9_-], compared at join time. A wrong token does not reject: it falls through to a knock, so rotating a token strands nobody. - Knock — the host answers per person, live. Knocks expire after 2 minutes, at most 10 wait at once, and two live participants can never share a name (attribution is by name).
-
Invite link —
cjam1_<base64url>, carrying a name, a per-invite secret, the addresses and an expiry. The daemon stores only a hash of each secret, in the 0700 state dir. A link is individually revocable, name-bound, expiring and countable (--uses). - Invite-only — knocks are refused outright, with the reason.
- Anything wrong with a link (tampered, expired, revoked, used up, name already connected) is
said out loud and falls through to a knock. A
cjam2_…link from a future format is a clean "update claude-jam", not a parse error.
Every guest-initiated action — a claude command, a file, an export, a permission answer — is
default deny, one request in flight per person, expiring after two minutes. always grants
standing approval that lives in daemon memory and dies with the daemon; it is never written
down.
/exit, /clear and /resume are a hard host-only list, enforced server-side. No guest
request path, no always, ever — they would end or wipe the session for everybody.
Exactly three of claude's own commands run for a guest with no round trip: /cost, /status,
/context, and only bare. /cost --json is an argument the allowlist has not read, so it asks.
A guest can answer a permission prompt, but never with raw keys:
- the daemon reads the prompt's numbered options off the screen;
- the guest asks for one digit;
- the host approves that digit;
- the daemon types that one digit, and only if the screen still says the same thing.
It refuses anything it cannot read cleanly (it wants the picker's own ❯ or a question line
above the options; a 10-option prompt is more than one digit can pick), and a prompt that changed
between the request and the approval is refused rather than answered. The free-text option
(Type something.) is raw keyboard access and stays the host's in every mode.
A question is not a permission. claude's own AskUserQuestion picker is a product decision,
so anyone may answer it outright. A tool asking to be allowed keeps the ladder above.
F3 (attach the real TUI) requires host and loopback — the client's own --host flag is a
label, not a permission. The same host-plus-loopback gate covers /end, claude-jam end's
POST /end, the invite endpoint and the relay switch, all of which additionally require the hook
secret out of the 0700 state dir. A secret never travels on an argv (an argv is in ps).
claude-jam ends only a tmux session it created: the session must carry the
@claude-jam-owned <state-dir> marker and that directory must hold a session.json
naming that exact session back. No name pattern, no filtered sweep over tmux list-sessions, no
kill-server, and --all re-verifies every session it touches. Since v0.20 that is structural
as well as checked — claude-jam's sessions live on their own tmux socket, so list-sessions
there cannot return one of yours even in principle.
Text reaches the TUI through tmux load-buffer + paste-buffer -p — a file, bracketed
paste, never a shell and never an argv. Everything a guest can send is either sanitized
(messages, chat, captions: control characters stripped, prefixes neutralised, capped at 20 000
characters) or gated (commands, keys, resize, transfers).
These are not boundaries. Do not plan around them.
-
"Never reveal the token or an invite link to a bridged participant" and "never claim to
have seen
/cchat" are sentences in claude's system prompt. A capable model usually follows them. A jailbreak, a confused turn, or a future model that summarises them away does not. What the appended system prompt buys is durability — it survives a/compactthat would have summarised the same words out of a hook — not enforcement. -
The token in claude's context. By default the daemon tells claude the token so it can help
a participant.
--no-token-in-contextkeeps it out. If a credential must not leak, run knock-only and mint invite links, rather than telling the agent a shared secret at all. -
/cchat is human-only because nothing forwards it, which is real — but the agent claiming not to have seen it is the instruction half.
-
Secret masking is a deny-list, not a scanner. It knows five shapes — AWS key ids, PEM
PRIVATE KEYblocks,sk-/gh?_-style tokens, bearer credentials,.env-style UPPER_CASEKEY=value— applied to tool calls, tool results,/diffoutput and every mirror row. Anything else goes through untouched, and on a mirror row a value split across colour escape sequences will not match. It is a seatbelt on the way out, not a boundary.[masked]on somebody's screen means a shape was recognised; the value is unchanged on disk and in claude's context. -
Export scrubbing is best effort. A transcript is everything claude saw — file contents,
tool output, the whole context. claude-jam strips its own token block and the raw token, and
nothing else. Run
/token newafter an export.
-
An invite link is a bearer credential. Whoever holds it is that person as far as
claude-jam is concerned. There is no second factor and no device binding. What you get instead
is per-person revocation, an expiry, a use count and a name binding — all of which the shared
--tokenlacks. Send links over a private channel. - Once in, everybody is equally trusted. An invite grants exactly the same abilities a knock does. There are no per-person capabilities.
-
Revoking or expiring a link disconnects nobody already in.
/kick <name> revokeis the pair that removes somebody and takes their link back. -
Bind is
0.0.0.0. Expose it over Tailscale, a LAN you trust, an SSH tunnel, or a relay whose URL you keep private — never a public IP you advertise. See Remote Access. -
The browser view is read-only but not anonymous: its URL contains the view key as HTTP
basic auth (
http://jam:<key>@host:7778), so the URL is the credential. -
bypassPermissionsremoves the whole permission half of this page. If the host's Claude Code runs with"defaultMode": "bypassPermissions"or--dangerously-skip-permissions, nothing ever asks — so there is no prompt to relay, no host approval, and every participant's message runs tools unattended. Do not share a session in that mode with anyone you would not hand the keyboard to.