Content
<div align="center">
# 🌉 session-bridge-mcp
**Let multiple Claude Code sessions talk to each other.**
One session writes straight into another's prompt over tmux, and every message
lands in a shared conversation log any session can review.
[](https://modelcontextprotocol.io)
[](https://bun.sh)
[](https://github.com/tmux/tmux)
[](#license)
</div>
---
## Why
You run one Claude Code session to **write** code and another to **review** the
PR — and you keep playing carrier pigeon between them. `session-bridge` removes
the pigeon. Each session lives in its own **tmux** pane under a **role**, and
they message each other directly.
The canonical setup is a 3-session loop:
```
┌─────────────────────────────────────────────────────────┐
│ gestor · talks to you, orchestrates the workflow │
└───────────────┬─────────────────────────┬────────────────┘
send │ │ send
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ dev │ ───────▶ │ reviewer │
│ writes code │ send │ reviews the PR │
└──────────────┘ ◀─────── └──────────────────┘
send
```
> `gestor` = manager. Rename the roles to whatever you like — they are just
> strings.
---
## How it works
Two transports, on purpose:
| Transport | What | Needs polling? |
|---|---|---|
| **PUSH** | `send` injects text into the peer's tmux pane (`set-buffer` → bracketed-paste → `Enter`). The peer receives it as a brand-new user turn. Claude Code even queues it if the peer is mid-task. | **No** — it's pushed. |
| **PULL** | `read_peer` captures what's currently on a peer's screen. Used to *watch* a session (e.g. the manager noticing the dev is asking the user something). | Yes — pair with the native `/loop` skill. |
Every message is **also** logged twice, so nothing is lost and any session can
catch up:
- `conversation.jsonl` — the single ordered cross-session thread.
- `mailbox/<role>.jsonl` — a per-role inbox (audit + fallback).
Bracketed paste (`paste-buffer -p`) means **multi-line messages, quotes and
whitespace survive intact** — the TUI won't treat embedded newlines as submits.
State lives under `~/.claude/session-bridge/`, so it is shared across every
Claude Code session on the machine and **survives `/clear`**.
```
~/.claude/session-bridge/
├── registry.json # role → tmux pane mapping
├── prompts.json # role → persistent system prompt (restored on /clear)
├── conversation.jsonl # global ordered thread
└── mailbox/
├── gestor.jsonl
├── dev.jsonl
└── reviewer.jsonl
```
---
## Install
Requires [Bun](https://bun.sh) and [tmux](https://github.com/tmux/tmux), and
Claude Code launched **inside** a tmux pane.
```sh
git clone git@github.com:c2developers/session-bridge-mcp.git
cd session-bridge-mcp
bun install
```
Register the server with Claude Code at **user** scope (available in every
project):
```sh
claude mcp add session-bridge -s user -- bun /abs/path/to/session-bridge-mcp/src/index.ts
```
Or drop it into a project's `.mcp.json` (see `.mcp.example.json`):
```json
{
"mcpServers": {
"session-bridge": {
"command": "bun",
"args": ["/abs/path/to/session-bridge-mcp/src/index.ts"]
}
}
}
```
Configure the state directory with `SESSION_BRIDGE_DIR` if you don't want the
default `~/.claude/session-bridge/`.
---
## Quickstart — 3 sessions in 60 seconds
```sh
# Three tmux sessions, each will run its own Claude Code
tmux new-session -d -s gestor
tmux new-session -d -s dev
tmux new-session -d -s reviewer
# In each one, launch `claude` and give it its role as the first message:
# gestor → "registra esta sesión como 'gestor' con session-bridge"
# dev → "registra esta sesión como 'dev' con session-bridge"
# reviewer → "registra esta sesión como 'reviewer' con session-bridge"
```
Each session calls `register(role)` once — it auto-detects its own pane from
`$TMUX_PANE`. After that they can reach each other by role name.
> **Bootstrap trick:** an already-registered session can register the others for
> you by injecting the instruction into their panes (the manager does exactly
> this). It just needs their pane ids from `tmux list-panes -a`.
---
## Tools
| Tool | Signature | Purpose |
|---|---|---|
| `register` | `(role, pane?)` | Register the current session under a role. Auto-detects the tmux pane from `$TMUX_PANE`; pass `pane` to override. Call once per session. |
| `list_peers` | `()` | All registered roles, their panes, cwd and last-seen time. |
| `send` | `(to, from, text, submit?, tag?)` | Push a message into a peer's prompt. `submit=false` types it without sending; `tag=false` drops the `[from:…]` prefix. Also logs to the thread + mailbox. |
| `read_peer` | `(role, lines?)` | Capture the peer's visible screen (default 50 lines of scrollback). |
| `detect_question` | `(role, lines?)` | Classify the peer's screen — `busy` / `waiting_select` / `waiting_text` / `idle` / `unknown` — and parse any open menu's options + heading. Lets the manager notice when a peer needs an answer. |
| `respond` | `(role, keys)` | Send raw control keys to drive an interactive menu/permission prompt, e.g. `['1']`, `['Down','Down','Enter']`, `['Escape']`. For prose, use `send`. |
| `history` | `(limit?, role?)` | Read the shared conversation, newest last. Filter by a role involved. |
| `inbox` | `(role, limit?)` | Read a role's mailbox — a poll/fallback if a pushed message was missed. |
| `set_prompt` | `(role, prompt)` | Save a persistent "system prompt" / briefing for a role. Survives `/clear` and restarts; auto-reinjected by the SessionStart hook. |
| `get_prompt` | `(role)` | Read back a role's saved prompt (`{found:false}` if none). |
| `list_prompts` | `()` | All saved role prompts. |
| `delete_prompt` | `(role)` | Forget a role's saved prompt so it stops being reinjected. |
### The watch loop (PULL) — answering a peer's questions
Receiving messages is push, so no loop is needed for that. A loop is only for
**watching** a peer's screen and noticing when it's stuck on a question
(plan-mode approval, a clarifying menu, a permission prompt). `detect_question`
classifies the screen so the loop doesn't bother a `busy` peer; `respond` then
drives the menu.
With Claude Code's native `/loop` skill, run this in the manager:
```
/loop 30s para 'dev' y 'reviewer' llama detect_question. Si state=='waiting_select'
o 'waiting_text', enséñame la pregunta y las opciones y decide la respuesta según el
flujo: contéstala con respond(role, keys) para menús (p.ej. ['1','Enter']) o con
send(...) para texto. Si la decisión es del usuario, párate y pregúntame. Si state
=='busy' o 'idle', no hagas nada.
```
> **Menus vs prose.** A menu/permission prompt is *not* a text turn — typing
> into it does nothing useful. Use `respond` with key tokens for those, and
> `send` only when the peer is genuinely waiting for free text.
### Sending slash commands across sessions
Slash commands are just text injected into the prompt:
```
send(to:'dev', from:'gestor', text:'/clear')
```
---
## Persistent role prompts — surviving `/clear`
`/clear` wipes a session's context — including *who it is* and how it should
behave. Re-typing each role's briefing every time is the pigeon problem all over
again. So a role's "system prompt" can be **saved once and restored automatically**.
1. **Save** a briefing per role (usually the `gestor` sets them up):
```
set_prompt(role:'dev', prompt:'You are the dev. Work on the assigned issue,
keep commits small, open a PR and ping the reviewer. Never merge yourself.')
```
It's written to `prompts.json` and survives `/clear`, restarts and reboots.
2. **Restore** happens on its own. A `SessionStart` hook fires on `/clear` (and
on startup/resume), reads the session's `$TMUX_PANE`, reverse-looks-up its
role in `registry.json`, and reinjects that role's saved prompt as
`additionalContext`. The session wakes up already knowing its identity — no
re-typing, no manual `get_prompt`.
Wire the hook once in **`~/.claude/settings.json`** (user scope, so it fires in
every session regardless of project):
```json
{
"hooks": {
"SessionStart": [
{
"matcher": "clear|startup|resume",
"hooks": [
{
"type": "command",
"command": "/abs/path/to/bun run /abs/path/to/session-bridge-mcp/src/session-start-hook.ts",
"timeout": 15
}
]
}
]
}
}
```
The hook is a **silent no-op** when it can't help: outside tmux, on an
unregistered pane, or when no prompt is saved for the role — so it never blocks a
session from starting. If a pane is bound to several roles (e.g. `dev` and
`developer`), the most recently `register`ed role wins.
> The prompt is restored as *context*, not as a literal model system prompt —
> Claude Code has no API to overwrite its own system prompt mid-session. In
> practice an injected role briefing at session start behaves the same way.
---
## Full workflow example
1. **You → gestor**: "implement issue #14".
2. **gestor → dev**: `send(to:'dev', from:'gestor', text:'Take issue #14: <summary>. Branch <domain>/14-slug. Ping the reviewer with the PR URL when ready.')`
3. **dev** codes. If it needs a user decision, the gestor (watching via `/loop` + `read_peer('dev')`) sees it and answers — or escalates to you.
4. **dev → reviewer**: `send(to:'reviewer', from:'dev', text:'PR #21 ready: <url>. Please review.')`
5. **reviewer** reviews → `send(to:'dev', …)` with changes, or `send(to:'gestor', …)` with "approved".
6. **dev** fixes → back to step 4. **reviewer** confirms → merges, closes issue/PR.
7. **reviewer → gestor**: "merged & closed". The gestor sends `/clear` to dev and kicks off the next task.
---
## Troubleshooting
| Symptom | Fix |
|---|---|
| `register` fails: `$TMUX_PANE is unset` | You launched Claude Code outside tmux. Start it inside tmux, or pass `pane` (`tmux display-message -p '#{pane_id}'`). |
| Message logged but `delivered:false` | The target pane died. Re-`register` that session (pane ids change when tmux restarts). The message is still in the mailbox. |
| Peer didn't react to a pushed message | It was busy; Claude Code queues it. Or check its `inbox(role)`. |
| MCP added mid-session but tools missing | Restart Claude Code — user-scope servers load at startup. |
| Prompt not restored after `/clear` | The session must have been `register`ed in that pane and have a saved prompt (`get_prompt(role)`). The hook only fires if wired in `settings.json`; check it with `/hooks` or run the script manually piping `{"source":"clear"}` with `TMUX_PANE` set. |
---
## Security
`send` injects text into another Claude Code prompt and `read_peer` reads its
screen. Anyone who can write to `~/.claude/session-bridge/registry.json`, or
drive your tmux server, can inject prompts into your sessions. **Run this only
on a trusted local machine.** There is no network listener — all coordination
is local files + the local tmux socket.
---
## License
MIT © c2developers
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available