-
Notifications
You must be signed in to change notification settings - Fork 0
Hosting a Jam
You keep the real claude TUI — your plugins, skills, MCP servers, CLAUDE.md, hooks and
account. You also join your own jam from a client, exactly like everybody else.
claude-jam host --name Roy --cwd ~/Code/some-project -- --model sonnetOr run claude-jam with no arguments and use the launcher menu — its Host screen collects the
same fields and prints the exact command line before it runs it.
That builds a detached tmux session (two windows: daemon, and claude — the real TUI) on
a tmux server of claude-jam's own, socket claude-jam-<port>, and opens your own client in the
terminal you launched from. Because it is claude-jam's own server it can bind a bare F3 to
detach-client without touching your tmux config, and it cannot see your own sessions at all.
claude-jam host --resume <id> restarts a conversation in a pane of claude-jam's own.
claude-jam adopt does not restart anything — it shares the claude that is already running,
in the tmux pane it is already in.
# from inside that session — claude can run this for you from the Bash tool
claude-jam adopt
# from another terminal, naming the pane (and its tmux server if it is not yours)
claude-jam adopt --pane %23 --socket default --token ourshared1It takes any claude-jam host flag as well, so claude-jam adopt --tunnel --jam-name friday
works. It shows what it resolved before it shares anything — the pane, the tmux server, what
is running in it, the directory, the session id it worked out, and that session's first message
and last answer — then asks. --yes skips the question for scripting, and refuses outright when
the transcript it picked looks stale: the failure this guards against is sharing the wrong
conversation with the room.
Not inside tmux? A bare terminal, an IDE terminal or a cmux pane has no pane to capture or
type into, so adoption is impossible. claude-jam says so and hands you the whole alternative with
the id already filled in: claude-jam host --resume <session-id> --cwd <dir>.
claude-jam did not create this session, so it will never end it. Adoption is the first time
claude-jam points tmux at a server it does not own, and the rule there is narrow and total: it
only READS (capture-pane, display-message, list-panes) and TYPES INTO THE ADOPTED PANE. No
new-session, no kill-session, no set-option — not the ownership marker, not the status line,
not the fill character — and no key binding, because a tmux key table is server-global and that
server is yours. The daemon runs in a tmux session of claude-jam's own, so claude-jam end
has something of its own to end: it stops the daemon and its children and leaves the pane, its
session and claude exactly as they were. claude-jam sessions marks the row adopted;
claude-jam clean never touches it.
At adoption claude is told it is now shared: one injected message, prefixed
[claude-jam:tool]: so it is visibly from the tool and not from a participant, carrying the
protocol, the two standing rules, the digest, who is here and where MANUAL.md is. It is re-sent
after a /compact or /clear — that is exactly when injected context disappears — and on a
meaningful roster change while the session is idle, at most one every ten minutes. --no-brief
skips the first one; --brief-updates off stops the later ones.
"The context went" is read off the pane, because a running claude cannot be given a hook, and
that is edge detection over what the screen looks like. A /compact and a /clear each leave
their own marker, so each fires. What does not fire is the same event twice with nothing
observable in between: /clear, then exchanges short enough that the screen never changes
signature, then /clear again reads as one. The roster re-brief is the backstop for that.
Two ceilings are inherent to adopting rather than starting, and are not worked around:
-
No Stop/Notification hooks. A running claude cannot be given new ones (
--settingsis read once, at startup), so turn-end and permission-wait come from the pane classifier — the screen, read 2.5 times a second — which is the authoritative source anyway. - The pane is not resized. It is your window, usually with you looking at it, so a guest on a much smaller terminal sees it letterboxed rather than reflowed.
integrations/claude-plugin/ in the repository is a three-file Claude Code plugin (a command, a
skill, a manifest — no code) mapping /jam, /jam invite <Name>, /jam end and /jam status
onto the CLI:
/plugin marketplace add roypadina/claude-jam
/plugin install claude-jam@claude-jamEntirely optional. claude-jam adopt from the Bash tool works without it. /jam is just
shorter, and the skill in the plugin carries the two standing rules to a claude that has not been
briefed yet.
Note what /jam with no argument does: claude-jam adopt run from the Bash tool has no terminal
to ask a yes/no question on, so it prints everything it resolved and stops. The command tells
claude to show you that block and wait for your confirmation before re-running with --yes.
| flag | default | what it does |
|---|---|---|
--name <X> |
Host |
how you appear to everyone, and to claude |
--cwd <dir> |
the current directory | where claude runs |
--port <N> |
7777 |
the WebSocket port. The state dir and tmux socket follow it |
--tmux <name> |
claude-jam |
the tmux session name — the identifier claude-jam end takes. A second jam auto-names claude-jam-2
|
--jam-name <X> |
this directory's name | what the jam is called: the welcome line, claude-jam sessions, /menu, and what people on your network see. Cosmetic — never used for auth or for a path |
--no-announce |
off (it announces) | do not announce this jam on the local network. See Security Model for what announcing discloses |
--token <value> |
none (knock-only) | 8–64 chars of [A-Za-z0-9_-]
|
--invite-only |
off | refuse knocks outright: an invite link is the only door |
--view |
off | also serve the TUI read-only in a browser (needs ttyd) |
--view-port <N> |
port + 1 | where that browser view listens |
--tunnel |
off | a Cloudflare quick tunnel (needs cloudflared) |
--funnel |
off | Tailscale Funnel instead (needs tailscale) — mutually exclusive with --tunnel
|
--replay <N> |
a default backlog | how many transcript events a joining guest is shown |
--history <N> |
2000 (cap 20000) |
how many events the jam keeps at all, for replay and for /history. --replay is how much of that a joining guest is shown |
--peer-tasks |
off | let your claude hand work to a guest's own Claude Code, on that guest's account and quota. Off unless you pass it, and even then nothing runs until that guest types /peer on and approves each individual task. See Peer Tasks
|
--answers host|anyone |
anyone |
who may /answer a question. Permissions are always yours |
--uploads ask|auto|off |
ask |
whether you are asked about every file a guest sends. See Files and Export — the caps never move |
--upload-quota <n>[MB|files] |
40 files / 200 MB | how much an auto session may take before it falls back to asking |
--export ask|auto|off |
ask |
the transcript's own toggle, deliberately separate — it does not follow --uploads
|
--no-sound |
off (it sounds) | start your client silent. The bell and the notification are separate toggles in /menu → Notifications
|
--config-dir <D> |
your default | run claude under another profile (CLAUDE_CONFIG_DIR) |
--resume <session-id> |
none | continue an existing claude session — it is restarted in a pane of claude-jam's own. To share one without restarting it, use claude-jam adopt
|
--no-system-prompt |
off | keep the shared-session contract in the hooks only |
--no-token-in-context |
off | never put the token in claude's context |
--no-popup |
off | no tmux display-popup approval prompts |
--tmux-socket <name> |
claude-jam-<port> |
default puts it on your shared tmux server (F3-out is then not bound) |
--attach |
— | reopen your client on a jam that is already running |
--no-attach |
off | do not open a client at all |
--no-prompt / --keep-on-exit / --end-on-exit
|
prompt | answer the on-exit question up front |
-- <claude args> |
— | everything after -- goes to claude verbatim |
claude-jam host --help prints the same flags in a shorter form, and /menu → Help & guides
lists them with the same descriptions. This table also carries a few that --help leaves out
because they are rarely wanted — --view-port, --no-popup, --no-token-in-context and
--no-attach.
Four doors, all switchable while the jam runs from /menu → Access. See
Security Model for what each one is actually worth.
| mode | how | who decides |
|---|---|---|
| invite link |
claude-jam invite Dana mints cjam1_…; claude-jam join <link> is the guest's whole command |
you, per person, in advance — and revocably |
| token |
--token <value>, or /token set later. One shared secret |
whoever holds the string |
| knock | no token. The guest waits; you get ⚑ Dana wants to join (100.86.8.97) and answer /accept Dana
|
you, per person, live |
| invite-only |
--invite-only, or /token invite-only on. Knocks are refused with a reason |
you, per link |
claude-jam invite Dana # multi-use, 24 h
claude-jam invite Dana --uses 1 --expires 30m # one shot, half an hour
claude-jam invites # id, name, state, uses, expiry — never the link again
claude-jam invite revoke Dana # or revoke <id>The same three from inside the client: /invite Dana, /invites, /invite revoke Dana.
Defaults are multi-use and 24 hours, because a guest whose laptop slept has to be able to come back. The daemon stores only a hash of each secret, in the 0700 state dir, and reloads them on restart — so a daemon that came back does not lock out the people it already invited. Anything wrong with a link (tampered, expired, revoked, used up, name already connected) is said out loud and then falls through to a knock, so nobody is silently rejected.
Every request — a knock, a guest's claude command, a file, an export, a permission answer — raises one row just above your status row:
⚑ Dana wants to join (100.86.8.97) · [a]ccept [d]eny [i]gnore · 2:00
counting down to that request's own expiry, with +N more when several wait. One key answers
it, and only while your input line is empty — the first character you type disarms the keys
until Esc, so a message starting with d can never deny anybody. Anyone attached to the tmux
session also gets a one-key tmux display-popup; whoever answers first wins.
Your client is the same client a guest gets, plus the host-only commands. The two keys that are yours alone:
-
F3 — attach the real TUI.
tmux attachtakes the terminal, so permission prompts, pickers, the mouse and Ctrl-C all work at native speed. F3 again (orCtrl-b d) comes back. Host and loopback only. -
adi/Esc — the approval bar, on an empty input line.
Host-only commands: /join, /token new|set|off, /token invite-only on|off,
/remote off|tunnel|funnel, /invite, /invites, /invite revoke, /kick <name> [revoke],
/end, /accept, /deny, /allow-cmd, /deny-cmd, /allow-perm, /deny-perm,
/allow-export, /deny-export, /accept-file, /deny-file, and /answer other <text>.
/menu is the live control panel: People · Invites · Access · Session · Notifications ·
Help & guides, with the jam's own state shown next to every toggle.
You are the only person who hears arrivals — a guest has nobody to approve — and the two sounds are different on purpose, so you know without looking whether somebody is waiting:
| moment | sound | repeats |
|---|---|---|
| a knock — they are waiting for you |
Submarine, a slow low tone, plus ⚑ <Name> wants to join
|
once after 30 s if nobody answers, then never |
| a token or invite-link join — they are already in |
Glass, one short chime, <Name> joined
|
no |
somebody nudges you (/ping, below) |
Hero |
only with /ping <Name> !
|
| somebody leaves | (silent — the roster line is enough) | — |
Three independent toggles in /menu → Notifications: sound, desktop notification,
terminal bell. --no-sound starts silent and /sound on|off flips it from the keyboard —
and it silences the sound only, so the line and the notification still arrive. On Linux the
sounds go through paplay (the freedesktop .oga theme most desktops ship) and fall back to
aplay (ALSA, .wav); a machine with neither set of files installed is silently skipped, which is
a correct answer and never an error. Note that nobody on this project has heard the Linux sounds —
which player and which file each of the three resolves to is tested, that they are distinguishable
by ear is not.
/ping <Name> [message] (alias /nudge) is available to everyone, you included — see
Joining a Jam. /who tells you who is
active, idle 4m or away 20m+ before you spend one.
claude-jam host --tmux work-jam --port 7779 --cwd ~/Code/otherEach jam gets its own port, its own state dir ($TMPDIR/claude-jam-<port>) and its own tmux
socket. claude-jam host on a name already held by one of claude-jam's own offers
[a]ttach as host · [n]ew session (claude-jam-2) · [e]nd it and start fresh · [c]ancel.
claude-jam sessions # what is running: name, port, state, uptime, who is here, cwd
claude-jam sessions --json # the same, for a script
claude-jam end [name] # end one. No name and one jam → that one; several → a numbered picker
claude-jam end --all # after an explicit confirmation, re-verifying every one
claude-jam clean # remove state dirs whose session is gone, and only those
claude-jam host --attach # reopen your client on a jam that is still running/end inside the client does the same, after really end this jam for everyone? [y/N].
Closing your client does not end the jam: you are asked
this jam is still running (2 guests connected) — [k]eep it running · [e]nd it · [c]ancel.
A stdin that is not a terminal counts as keep, so nothing destructive happens because nobody
was there to answer.
An adopted jam ends differently, and that is the point. claude-jam end on one stops the
daemon and its children and leaves the pane, its tmux session and claude exactly as they were —
claude-jam did not start that session, so it never ends it. The row says adopted, and
claude-jam clean leaves it alone.
claude-jam only ever ends a tmux session it created. On creation it stamps
@claude-jam-owned <state-dir> on the session and writes session.json into that directory;
ending anything requires that pair to line up for the exact name you gave. There is no name
pattern, no filtered sweep over tmux list-sessions, no kill-server, and --all re-verifies
every session it touches. A session started by claude-jam 0.18.0 carries the older @jam-owned
marker and is still recognised — that name is read as well as the new one.
By default your jam announces itself on the local network, so a guest on the same wifi runs
claude-jam find (or just claude-jam join) and picks it out of a list instead of waiting for
you to send an address. What is announced is the jam's name, your display name, eight characters
of the session id, which kind of door it is, whether a browser view exists, and the version —
never the token, an invite secret or any path.
It is still a disclosure: everyone on that network learns the jam exists, what it is called and
who is hosting it. On your own network that is the point; on café wifi it is not. --no-announce
at launch, or /menu → Access → Announce on the network while it runs, turns it off — and
remember the default name is this directory's name, so --jam-name "friday" is worth typing
before a client name goes on somebody else's wifi.
Finding a jam is never entering one: whichever door you set up is still the door. Full detail in Security Model.
Joining a Jam · Remote Access · Files and Export · Security Model · Troubleshooting