Skip to content

c2developers/session-bridge-mcp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

3 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸŒ‰ 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.

MCP Runtime tmux License


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 and tmux, and Claude Code launched inside a tmux pane.

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):

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):

{
  "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

# 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):

{
  "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 registered 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 registered 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

About

MCP server that lets multiple Claude Code sessions talk to each other over tmux (gestor/dev/reviewer workflow)

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors