Set up a client machine
Each machine that runs Claude Code sessions needs a checkout of this repo (for the helper script and the shim), its own token, and two MCP entries. The tested path keeps the token in 1Password.
1. Check out and build
git clone https://github.com/catesworks/session-bus.git ~/src/session-bus
cd ~/src/session-bus
corepack enable
pnpm install --frozen-lockfile && pnpm run build
Use a stable location. The MCP entries hold absolute paths into it.
2. Mint a token on the bus host and store it in 1Password
On the bus host, once per client machine. The token goes from token mint straight into op item create through a pipe. It never appears in argv or in a shell variable.
node dist/cli.js token mint --principal me --scopes bus:read,bus:write --label laptop \
| node -e 'let t="";process.stdin.on("data",d=>t+=d).on("end",()=>process.stdout.write(JSON.stringify({title:"session-bus/laptop",category:"API_CREDENTIAL",fields:[{id:"SESSION_BUS_TOKEN",label:"SESSION_BUS_TOKEN",type:"CONCEALED",value:t.trim()}]})))' \
| op item create --vault <vault>
- Pass the JSON on stdin and do not add
--template.op item create --template -fails with "cannot create an item from template and stdin at the same time". - Name the field exactly
SESSION_BUS_TOKEN. - Note the item id that
opprints. Use the id, not the title, inop://references. A title containing/breaksop://lookups. - If your default
oplogin cannot see the vault, prefix the command withOP_SERVICE_ACCOUNT_TOKEN="$<VAR>", where<VAR>holds a service-account token that can.
3. Add the entries
On the client machine:
scripts/setup-client.sh --url https://<bus-host>.<tailnet>.ts.net:8443 --item <item-id> --vault <vault> --op-sa-var ''
Preview first with --dry-run. It prints two entries:
bus {"type":"http","url":"https://<bus-host>.<tailnet>.ts.net:8443/mcp","headersHelper":"SESSION_BUS_OP_REF=op://<vault>/<item-id>/SESSION_BUS_TOKEN /path/to/session-bus/scripts/bus-headers.sh"}
bus-push {"type":"stdio","command":"/path/to/node","args":["/path/to/session-bus/dist/cli.js","channel"],"env":{"BUS_URL":"https://<bus-host>.<tailnet>.ts.net:8443","SESSION_BUS_TOKEN_CMD":"op read op://<vault>/<item-id>/SESSION_BUS_TOKEN"}}
busis the pull entry. ItsheadersHelperruns through a shell each time Claude Code connects, so theop://reference is resolved then. The config holds no secret.bus-pushis the push shim. Without a session name it idles, so it is safe in everyclaude.- With
--op-sa-var <VAR>instead of'', both commands are prefixed withOP_SERVICE_ACCOUNT_TOKEN="$<VAR>". The variable name stays literal in the config and is expanded when the command runs. --allow-toolsaddsmcp__bustopermissions.allow, so bus tool calls do not prompt.
The script then checks that both entries are Connected and that this prints 0:
grep -c sbus_ ~/.claude.json
In /mcp, bus should show 7 tools.
Never store the expanded token
Do not use claude mcp add ... -e SESSION_BUS_TOKEN=<token> or --header "Authorization: Bearer <token>". Both write the token into ~/.claude.json and your shell history.
If you cannot use 1Password, put ${SESSION_BUS_TOKEN} (the literal text) in a project .mcp.json and export the variable in the shell that launches claude. Whether user-scope entries expand ${VAR} has not been tested.
The skill
skills/session-bus/ teaches a session how to pick a name, send, read, ack, and treat incoming messages as untrusted. Install it with a symlink:
ln -s "$PWD/skills/session-bus" ~/.claude/skills/session-bus
Its allowed-tools lets the 7 bus tools run without prompting while the skill is active. The same skill is available as /sb:session-bus once this repo is added as a Claude Code plugin marketplace.
Second machine checklist
- Network: the machine can reach the bus (an unauthenticated
POSTreturns 401):curl -s -o /dev/null -w '%{http_code}\n' -X POST https://<bus-host>.<tailnet>.ts.net:8443/mcp - Own token item, own
setup-client.shrun. - A
sendfrom the first machine appears in this machine'sinbox.