claude-session-bridge
by devpark435
README.md
<div align="center">
[](README.md)
[](README.ko.md)
[](README.zh.md)
[](README.ja.md)
[](https://www.npmjs.com/package/claude-session-bridge)
[](https://github.com/devpark435/claude-session-bridge)
</div>
# claude-session-bridge
**Let two (or more) Claude Code sessions talk to each other — and optionally
orchestrate themselves.**
You open one Claude Code session for your **backend** and another for your
**frontend**. Normally they know nothing about each other — when the backend
changes an API, you copy-paste the result into the frontend session by hand.
This tool removes the copy-paste. A session **sends** its result; sibling
sessions **receive** it automatically. Turn on the optional auto mode and a
finished backend can **wake** the frontend to react — hands-off ping-pong —
while you keep full control of the safety rails (no auto-commits, deploy gates,
loop guards) and change every setting from inside Claude.
> New here? Read top to bottom — every command is included. You don't need to
> understand the internals to use it.
---
## Table of contents
- [How to picture it](#how-to-picture-it)
- [Requirements](#requirements)
- [Install (one time)](#install-one-time)
- [Starting sessions with `.bridge.json` + `up`](#starting-sessions-with-bridgejson--up)
- [Connect two sessions](#connect-two-sessions)
- [Two ways to set the variables](#two-ways-to-set-the-variables)
- [Using tmux](#using-tmux)
- [Send and receive messages](#send-and-receive-messages)
- [Roles are just labels](#roles-are-just-labels)
- [Run several projects at once](#run-several-projects-at-once)
- [Auto mode: the event spawner](#auto-mode-the-event-spawner)
- [Drivers: auto / tmux / resume / spawn](#drivers-auto--tmux--resume--spawn)
- [Configure and run](#configure-and-run)
- [Safety rails](#safety-rails)
- [Sensitive roles are off by default](#sensitive-roles-are-off-by-default)
- [Never auto-commit (and how to allow it)](#never-auto-commit-and-how-to-allow-it)
- [Be careful with auto-deploy](#be-careful-with-auto-deploy)
- [Change settings from inside Claude](#change-settings-from-inside-claude)
- [Reference](#reference)
---
## How to picture it
Two simple ideas:
- **Project = a chat room.** Sessions launched with the same project name are in
the same room and can talk. Different project name = different room, isolated.
- **Role = your nickname in that room.** `backend`, `frontend`, `infra` — any
label you like. You address messages to a role.
```
Session 1 project "shop" role "backend" ┐
├─ room "shop" (they talk)
Session 2 project "shop" role "frontend" ┘
Session 3 project "blog" role "backend" ─── room "blog" (separate, isolated)
```
There is **no "connect" button.** Launching two sessions with the same project
name *is* connecting them.
---
## Requirements
- **Node.js 18+** — check with `node --version`
- **Claude Code** (the `claude` command) — check with `claude --version`
- **tmux** — optional. The default `auto` driver uses it to reach a role's
live pane when one is open, but doesn't require it: without tmux, `auto`
falls back to resuming or spawning a headless session instead. Only the
`tmux` driver itself (forcing live-pane-only wakes) needs tmux installed.
---
## Install (one time)
You do this **once.** Afterward you never edit these files again — the only
thing that changes per session is two environment variables (explained next).
**Recommended: install via `npx`, no clone needed.** Run this **inside a
Claude Code session** (so it targets that session's config dir — see the
note below):
```bash
npx claude-session-bridge install # registers the MCP server + hooks
session-bridge doctor # verify what's set up, and where
```
`install` copies the package runtime to `~/.claude/bridge/runtime/<version>/`
and registers *that* path in settings.json — not the npx cache, which npm
prunes periodically and would silently break the MCP server. It also merges
the **receive** hook, the **block-git** hook (blocks `git commit`/`push`/
`reset`), and a **SessionStart/SessionEnd** hook pair (records this session's
live status, cwd, tmux pane, and session id — so the `resume`/`auto` drivers
know what to wake later) into your active settings.json — idempotently,
without touching your other settings. Add `--no-block-git` if you *want*
sessions to commit on their own.
To remove everything `install` added:
```bash
session-bridge uninstall # removes our MCP server + hook entries only
session-bridge uninstall --purge --yes # also wipes the bus: messages, session registry, runtime copies
```
`uninstall` leaves every other setting untouched — it only strips entries it
recognizes as its own.
> **Work accounts / multiple profiles.** Claude Code reads
> `$CLAUDE_CONFIG_DIR/settings.json` when that env var is set (common for a
> separate work login), otherwise `~/.claude/settings.json`. This is the #1
> reason "I edited settings and nothing changed" — you edited the wrong file.
> Running `session-bridge install` **inside a session of a given profile**
> targets the correct file automatically. Run it once per profile. Check with
> `session-bridge doctor`, which prints the active config dir.
<details>
<summary>Developer / contributor install (clone + build, no runtime copy)</summary>
If you're working on this repo itself, install from your local checkout
instead so settings.json points at your `dist/`, not a copied runtime:
```bash
git clone <this-repo-url> claude-session-bridge-mcp
cd claude-session-bridge-mcp
npm install
npm run build
npm link # optional: puts `session-bridge` on PATH
session-bridge install --no-copy # registers this repo's dist/ directly
session-bridge doctor
```
`--no-copy` is also the flag `install` uses internally when it detects a dev
checkout without a published version to copy from.
</details>
<details>
<summary>Manual settings.json (if you'd rather edit by hand)</summary>
Add to the right settings.json (replace `/ABSOLUTE/PATH`):
```json
{
"mcpServers": {
"session-bridge": {
"command": "node",
"args": ["/ABSOLUTE/PATH/claude-session-bridge-mcp/dist/server.js"]
}
},
"hooks": {
"UserPromptSubmit": [
{ "hooks": [ { "type": "command",
"command": "node /ABSOLUTE/PATH/claude-session-bridge-mcp/dist/hooks/recv.js" } ] }
],
"PreToolUse": [
{ "matcher": "Bash", "hooks": [ { "type": "command",
"command": "node /ABSOLUTE/PATH/claude-session-bridge-mcp/dist/hooks/block-git.js" } ] }
]
}
}
```
</details>
All hooks no-op safely in non-bridged sessions, so they're fine installed
globally. That's the whole setup.
---
## Starting sessions with `.bridge.json` + `up`
Setting `BRIDGE_PROJECT`/`BRIDGE_ROLE` by hand (next section) works anywhere,
but if your roles always live in the same repo paths, declare them once and
let `up` do the typing. Drop a `.bridge.json` in your repo root:
```json
{
"project": "shop",
"roles": {
"backend": { "cwd": "." },
"web": { "cwd": "../shop-web", "model": "sonnet" }
}
}
```
`cwd` is resolved relative to the `.bridge.json` location (a role without it
runs in that same directory); `model` and `permissionMode` are optional and
seed the spawner's per-role config the first time `up` runs that role.
Discovery walks up from your current directory like git does, so `up` works
from anywhere inside the repo.
Launch one role:
```bash
session-bridge up backend # spawns `claude` with BRIDGE_PROJECT/BRIDGE_ROLE set
session-bridge up backend --admin # same, but grants BRIDGE_ADMIN=1 (can change spawner settings)
```
Launch every declared role at once:
```bash
session-bridge up
```
Inside tmux, this splits one new pane per role (your current pane is left
alone) and tiles the layout — each pane's session auto-registers via the
SessionStart hook. Outside tmux, it just lists the `session-bridge up <role>`
commands to run in your own terminals.
The env-var method below still works and is the right choice when roles
don't map to fixed local paths (e.g. spawned/remote checkouts).
---
## Connect two sessions
To put a session in a room, give it two values **when you launch it**:
| Variable | Meaning | Example |
|----------|---------|---------|
| `BRIDGE_PROJECT` | the room name | `shop` |
| `BRIDGE_ROLE` | this session's nickname | `backend` |
```bash
# Terminal 1 — the backend of project "shop"
BRIDGE_PROJECT=shop BRIDGE_ROLE=backend claude
# Terminal 2 — the frontend of project "shop"
BRIDGE_PROJECT=shop BRIDGE_ROLE=frontend claude
```
Both used project `shop`, so they're in the same room and can talk.
### Two ways to set the variables
Pick one — **but don't mix them up; this is the #1 beginner mistake.**
**Way A — on the same line (simplest, recommended):**
```bash
BRIDGE_PROJECT=shop BRIDGE_ROLE=frontend claude
```
The variables apply only to that one `claude`. Nothing to clean up.
**Way B — set first, then launch.** On separate lines you **must** use `export`:
```bash
export BRIDGE_PROJECT=shop
export BRIDGE_ROLE=frontend
claude
```
> ⚠️ **The trap:** `BRIDGE_PROJECT=shop` on its own line **without** `export`
> sets a shell variable `claude` will **not** see — the bridge silently won't
> connect. Use `export` (Way B), or put everything on one line (Way A).
**Tip — make aliases** so you don't retype this. In `~/.zshrc` (or `~/.bashrc`):
```bash
alias shop-back='BRIDGE_PROJECT=shop BRIDGE_ROLE=backend claude'
alias shop-front='BRIDGE_PROJECT=shop BRIDGE_ROLE=frontend claude'
```
### Using tmux
tmux changes nothing about launching — each pane is its own shell, like a
separate terminal. Set the variables in each pane and run `claude`:
```
tmux
├ pane 1: BRIDGE_PROJECT=shop BRIDGE_ROLE=backend claude
├ pane 2: BRIDGE_PROJECT=shop BRIDGE_ROLE=frontend claude ← same room as pane 1
└ pane 3: BRIDGE_PROJECT=blog BRIDGE_ROLE=backend claude ← different room
```
tmux matters most for **auto mode**: when a pane is open, the `auto` (default)
and `tmux` drivers wake it directly (see [the spawner](#auto-mode-the-event-spawner)).
A session launched inside tmux registers its pane on its own via the
SessionStart hook — no manual setup. Skip tmux entirely and `auto` still
automates things; it just resumes or spawns a headless session instead of
nudging a live pane.
> If you use `export` (Way B) in a pane and later launch a *different* project's
> `claude` in that **same** pane, the old `export` lingers. Prefer Way A inside
> tmux to avoid surprises.
---
## Send and receive messages
Once two sessions share a room, this just works:
1. In the **backend** session, when you have something worth sharing:
> "Send the new `/users` response shape to the frontend session."
It calls the `bridge_send` tool to deliver the message.
2. In the **frontend** session, the next time you type anything, the message is
**automatically added** to its context — no copy-paste.
That's the default ("bus") mode: **sending is deliberate** (the model shares a
real result, not a wall of text), and **receiving is automatic** on the next
turn. You're still driving.
You can also be explicit in either session: *"Check the bridge for new
messages"* (`bridge_recv`) or *"Send this to infra: staging is ready"*
(`bridge_send`).
> If a session finishes and has nothing worth sharing, it simply doesn't send —
> nothing hits the bus and any ping-pong ends naturally. Idle sessions don't
> spam the room.
---
## Roles are just labels
A role is **any string you want** — not only `backend`/`frontend`. Add as many
sessions to a room as you like:
```bash
BRIDGE_PROJECT=shop BRIDGE_ROLE=backend claude
BRIDGE_PROJECT=shop BRIDGE_ROLE=web claude
BRIDGE_PROJECT=shop BRIDGE_ROLE=infra claude
BRIDGE_PROJECT=shop BRIDGE_ROLE=qa claude
```
When sending, address a specific role (`to: "infra"`) or broadcast to everyone
else (`to: "*"`). A message addressed to `frontend` is delivered **only** to
`frontend` — `infra`/`qa` never even see it, so they can't act on it by mistake.
(Only `to: "*"` reaches everyone.) So a multi-step flow is natural:
> backend finishes an API → tells **web** → web updates the UI → tells **infra**
> → infra redeploys.
Whether that chain runs **automatically** is up to the spawner below — and for
deploys, read [Be careful with auto-deploy](#be-careful-with-auto-deploy) first.
---
## Run several projects at once
Use different project names; they never see each other's messages:
```bash
BRIDGE_PROJECT=shop BRIDGE_ROLE=backend claude # room "shop"
BRIDGE_PROJECT=blog BRIDGE_ROLE=frontend claude # room "blog" — isolated
```
Isolation is structural (messages live under a per-project folder), so `shop`
and `blog` can't cross over.
---
## Auto mode: the event spawner
Everything above keeps **you** in the loop — a session acts only when you give
it a turn. The **event spawner** is an optional background program (a daemon)
that removes that step: when a role receives a message, the spawner **wakes that
role automatically** to act on it, so the sides can ping-pong with no human in
between. You run it in its own terminal; it's opt-in and easy to turn off.
### Drivers: auto / tmux / resume / spawn
The **driver** decides *how* a role is woken:
| | **auto** (default) | **tmux** | **resume** | **spawn** |
|---|---|---|---|---|
| What it does | Per role: live tmux pane → nudge it; else a stored session id → resume it headlessly; else fresh spawn (its returned session id is stored, so the *next* wake resumes) | Types into your **already-open live session** (`tmux send-keys`) | Headless `claude -p --resume <id>` — continues the same conversation (falls back to a fresh spawn if no id is on record yet) | Fresh `claude -p` process every event, always |
| Context | Preserved, whichever path it takes | Preserved — the same live session continues | Preserved — same conversation | None — the spawn driver never resumes, even once an id exists |
| Uses `claude -p`? | Only when it falls back to resume/spawn | No | Yes | Yes |
| Needs tmux? | No — uses it when a live pane is registered, works fine without | Yes | No | No |
One rule `resume` shares with `auto`: neither ever resumes a session that is
**currently open interactively** — resuming a live conversation from the side
would clash with it, so those messages just arrive via the recv hook on that
session's next turn.
Analogy: **tmux** taps an employee already at their desk; **resume** calls a
contractor back and hands them the file they left off on; **spawn** hires a new
temp who starts from scratch every time. **auto** is the dispatcher — try the
desk, then the returning contractor, then a new temp — and it remembers who to
call next time.
Why `auto` is the default: it gets tmux's benefits (watch, interrupt, no
`claude -p`) whenever a pane is open, and falls back to a *continuing* headless
conversation — not a cold start — when it isn't. tmux is no longer required to
get context-preserving automation. Pick one of the other drivers only if you
want a single behavior everywhere: `tmux` to force live-pane-only (skip roles
with no open pane), `resume` to always continue headlessly, `spawn` for a
guaranteed clean slate on every wake.
### Configure and run
A role is auto-woken once it has a target — either a configured `cwd`, or a
session record left behind by any bridged session that has run there before
(hooks installed is enough; no explicit `spawner set` required):
```bash
# tmux / auto drivers: just open the role's session inside tmux — its pane
# (and session id) auto-register via the SessionStart hook.
# resume / spawn drivers, or auto with no pane open yet: tell it where the
# role's code lives:
session-bridge spawner set shop backend cwd=/path/to/backend
session-bridge spawner on # enable globally
session-bridge spawner off shop # ...or per project
session-bridge spawner off shop web # ...or one role
session-bridge spawner status # inspect current config
```
(`resume`/`auto` can also pick up `cwd` automatically from a role's session
record once any session has run there — set it manually only for a role that's
never been opened, or to force the `spawn` driver's target.)
(`session-bridge` is the CLI installed with this package. If it's not on your
`PATH`, run `node /ABSOLUTE/PATH/dist/cli.js spawner ...`.)
Run the daemon in its own terminal:
```bash
session-bridge spawner run # foreground; Ctrl-C to stop
# or background it:
nohup session-bridge spawner run > ~/.claude/bridge/spawner.log 2>&1 &
```
Config is re-read on every event, so `on`/`off` and other changes take effect
**live** — no restart needed.
### Safety rails
| Rail | Default | What it does |
|------|---------|--------------|
| `maxHops` (loop guard) | 6 | A per-project chain counter caps consecutive auto-wakes, resetting after a quiet gap. Bounds runaway ping-pong (works for every driver). |
| `rateLimitPerMinute` | 12 | Max auto-wakes per role per minute. |
| sensitive roles | `infra`, `qa` | Never auto-woken unless explicitly enabled — a misrouted message can't trigger a deploy. |
| no auto-commit | on | `git commit`/`push`/`reset` blocked for headless drivers (spawn/resume, and auto whenever it wakes headlessly); live/tmux sessions need the block-git hook (see below). |
| single-flight / cooldown | — | One live run per role at a time for headless wakes (spawn, resume, or auto's headless fallback); a re-nudge cooldown for tmux nudges (tmux driver, or auto's live-pane path). |
| target required | — | The `tmux` driver never wakes a role with no live pane; `spawn`/`resume` (and auto's headless fallback) need a `cwd` from config or a prior session record, or the role is skipped. |
| `staleSessionHours` | 24 | A live-flagged session record older than this is treated as crashed, so `resume`/`auto` may take it over instead of deferring to it. |
### Sensitive roles are off by default
`infra` and `qa` ship in `defaultOffRoles` — the spawner **will not auto-wake
them even when configured.** You must opt them in by name.
**Why?** The sender chooses *who* a message goes to, and the sender is a language
model. If it ever broadcasts (`to: "*"`) or mis-addresses, `infra`/`qa` could
receive a request not meant for them — and those roles do the riskiest things
(deploys, releases, destructive tests). Keeping them off is a guarantee that
does **not** depend on the model addressing correctly: a misrouted message just
sits in the inbox until a human opens that session.
**Turn them on (if you want full automation):**
```bash
session-bridge spawner on shop infra # explicit opt-in (required)
```
Read [Be careful with auto-deploy](#be-careful-with-auto-deploy) first. To
change the list, edit `defaultOffRoles` in `<BRIDGE_ROOT>/spawner.config.json`.
### Never auto-commit (and how to allow it)
By default, auto-driven sessions can't commit. **Two layers** enforce it:
1. **Headless drivers (spawn, resume — and auto whenever it falls back to
either):** every headless launch adds
`--disallowed-tools "Bash(git commit:*)" "Bash(git push:*)" ...`. Deny rules
beat allow/bypass, so the model physically can't commit.
2. **Live sessions (tmux driver, or auto's live-pane path):** a live session's
turn isn't launched with `--disallowed-tools`, so install the
**`block-git` PreToolUse hook** (in [Install](#install-one-time)). It denies
`git commit`/`push`/`reset` in any session, however its turn was triggered.
> A `CLAUDE.md` reminder ("don't commit") is a nice-to-have but **not enough**
> for autonomous loops — a model can drift. The hook/deny rules are the real
> guarantee.
**If you WANT commit/push automation** (some people do), opt out:
- Don't install the `block-git` hook, **and**
- Remove git entries from `denyTools` (`session-bridge spawner` config) — it
applies to every headless launch (spawn and resume alike).
- Or narrow what's blocked with `BRIDGE_BLOCK_GIT` (a regex; e.g. allow commit
but still block push).
> ⚠️ Auto-`push` is outward-facing and hard to undo. If you allow it, push to a
> **feature branch** and protect `main` (branch protection). Don't auto-push to
> `main`.
### Be careful with auto-deploy
⚠️ Commits/pushes are blocked — but **deploying is not git.** `kubectl apply`,
`vercel deploy`, `terraform apply`, an SSH deploy script — none are blocked by
default, so an autonomous `infra` session could run them. Deploying is
outward-facing and hard to undo; doing it with no human present is risky.
If you want `infra` in the loop, pick one:
1. **Keep infra manual** (default — it's off). Messages queue; you open the
session and deploy yourself.
2. **Block deploy commands** (add them to `denyTools` or the block-git regex).
3. **Automate only safe steps** — staging/dry-run automatic, production manual.
---
## Change settings from inside Claude
You don't have to edit JSON or remember CLI flags. The MCP server lets you read
and change every automation setting **from inside a Claude session** — a
dashboard, conversational or via `/mcp`.
**Safety: admin-gated.** Settings can only be *changed* from a session launched
with `BRIDGE_ADMIN=1` (your "control" session). Auto-driven sessions don't have
it, so a runaway loop can't flip its own safety settings. Reading config is
always allowed.
```bash
# your control / dashboard session
BRIDGE_PROJECT=shop BRIDGE_ROLE=control BRIDGE_ADMIN=1 claude
```
**Two ways to drive it:**
**A. Just talk to it** (the model calls the tools):
> "Show the bridge config." · "Set frontend to auto." · "maxHops 3, driver
> tmux." · "Turn automation off."
**B. `/mcp` prompts** (slash commands that apply the change on the spot):
```
/mcp__session-bridge__show-config show current settings
/mcp__session-bridge__set-mode project, role, auto|manual
/mcp__session-bridge__configure-role cwd / model / tmuxTarget
/mcp__session-bridge__set-driver auto | tmux | resume | spawn
/mcp__session-bridge__automation on | off (master switch)
/mcp__session-bridge__set-limits maxHops / rateLimitPerMinute
```
Pick one, fill the arguments, and it writes the change immediately (admin
session only). The daemon picks it up live.
---
## Reference
### Tools (callable by a session)
| Tool | Admin? | Purpose |
|------|--------|---------|
| `bridge_send(to, body)` | no | Send to a role, or `"*"` to broadcast |
| `bridge_recv()` | no | Pull + consume unread messages |
| `bridge_peek()` | no | Preview unread without consuming |
| `bridge_tail(limit?)` | no | Inspect recent messages |
| `bridge_roles()` | no | List roles in the project |
| `bridge_whoami()` | no | Show this session's project + role |
| `bridge_config()` | no | Show automation settings |
| `bridge_mode(project, role, auto\|manual)` | **yes** | Set a role's mode |
| `bridge_set(project, role, {...})` | **yes** | Configure a role (cwd/model/tmuxTarget/permissionMode) |
| `bridge_settings({...})` | **yes** | Global knobs (driver/maxHops/rate/…) |
### CLI
```bash
# setup (targets the active config dir: $CLAUDE_CONFIG_DIR or ~/.claude)
session-bridge install [--no-block-git] [--config-dir <dir>]
session-bridge doctor
# inspect the bus (use any role as your identity)
BRIDGE_PROJECT=shop BRIDGE_ROLE=cli session-bridge roles
BRIDGE_PROJECT=shop BRIDGE_ROLE=cli session-bridge send frontend "hello"
BRIDGE_PROJECT=shop BRIDGE_ROLE=cli session-bridge tail 20
session-bridge root # bus storage directory
# the spawner
session-bridge spawner set <project> <role> cwd=<path> [model=<m>] [tmuxTarget=<t>]
session-bridge spawner on [project] [role]
session-bridge spawner off [project] [role]
session-bridge spawner status
session-bridge spawner run [--replay]
```
### Environment variables
| Var | Required | Default | Meaning |
|-----|----------|---------|---------|
| `BRIDGE_PROJECT` | yes | — | Project (room) name — the isolation boundary |
| `BRIDGE_ROLE` | yes | — | This session's role (nickname) |
| `BRIDGE_ADMIN` | no | off | `1` lets this session change automation settings |
| `BRIDGE_ROOT` | no | `~/.claude/bridge` | Where messages/config are stored |
| `BRIDGE_BLOCK_GIT` | no | `commit\|push\|reset` | Regex of git ops the block-git hook denies |
| `BRIDGE_AUTOSEND` | no | off | `1` enables the Stop auto-send hook |
| `BRIDGE_SEND_TO` | no | `*` | Default recipient for auto-send |
| `BRIDGE_HOP` | no | `0` | Loop-guard hop (set on every headless launch — spawn or resume) |
### Optional: auto-send every turn
Off by default — deliberate `bridge_send` is preferred. To broadcast a session's
last message automatically each turn, set `BRIDGE_AUTOSEND=1` and add the Stop
hook to `settings.json` (`dist/hooks/send.js`). Override the target with
`BRIDGE_SEND_TO`. Noisier and more tokens than deliberate sends.
### How it works (for the curious)
Messages and config are plain files under `BRIDGE_ROOT`:
```
~/.claude/bridge/
spawner.config.json automation settings (edited live)
<project>/
<role>.inbox.jsonl append-only log of messages for that role
.cursors/<role>.cursor how many messages that role has read
.sessions/<role>.json session record — id/cwd/tmux pane/live flag,
kept fresh by the SessionStart/SessionEnd hooks;
the tmux/resume/auto drivers read it to find
a live pane or a session id to resume
```
`bridge_send` appends to the recipient's inbox. The receive hook reads your
inbox from your cursor forward, injects the new lines, and advances the cursor —
so you never see a message twice. Isolation is just the per-project folder.
### Tests
```bash
npm test # MCP bus end-to-end
npm run test:config # config dashboard + admin gate
npm run test:prompts # /mcp prompts apply changes (admin-gated)
npm run test:spawner # spawn-driver ping-pong + loop guard
npm run test:tmux # tmux-driver ping-pong + loop guard
npm run test:real # real two-session test using `claude -p`
```
### License
MIT
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues