Skip to main content
Glama
exalsch

claude-session-bus

by exalsch

claude-session-bus

A tiny Claude Code plugin that lets separate Claude sessions working the same project pass each other short messages: "holding the dev server on :1420", "editing dynamics.rs", "branch X pushed, safe to rebase".

  • Send = an MCP tool (broadcast) the calling session invokes.

  • Receive = a UserPromptSubmit hook that prepends new peer messages to the other session's context before its next turn.

  • Transport = append-only JSONL mailboxes, one per scope.

Broadcast-to-all, one-way, fire-and-forget. Advisory: it announces, it does not enforce. The sender picks how far each message travels - the same project, a group of projects, or every session on the machine.

Full design: docs/specs/2026-06-29-claude-session-bus-design.md.

Layout

claude-session-bus/
├── .claude-plugin/
│   ├── plugin.json         # plugin manifest: hooks + mcpServers
│   └── marketplace.json    # lets you add this dir as a local marketplace
├── server/index.mjs        # MCP stdio server: broadcast(), inbox(), set_group()
├── scripts/drain.mjs       # UserPromptSubmit hook (the receive half)
├── scripts/selftest.mjs    # offline smoke test of the mailbox primitives
├── lib/bus.mjs             # shared: boxes, group registry, identity, locking, message shape
├── hooks/hooks.json        # declares the UserPromptSubmit hook
└── package.json            # type:module, no runtime deps

Related MCP server: claude-intercom

Scopes

Every broadcast takes a scope. Each scope is a separate mailbox with its own sequence numbers and per-session cursors, so the feeds never interfere:

scope

reaches

mailbox

project (default)

sessions in this project, git worktrees included

<repo root>/.claude/.session-bus/

group

sessions in every project that joined this project's group

~/.claude/.session-bus/groups/<name>/

global

every Claude Code session on this machine

~/.claude/.session-bus/global/

Git worktrees share the main checkout's box. A session in <repo>/.claude/worktrees/feat-x resolves to <repo> via git rev-parse --git-common-dir, so it reaches the main checkout and every sibling worktree at plain project scope, and its group membership is inherited rather than needing a set_group call per throwaway directory. Working from a subdirectory of a repo resolves the same way. A directory that is not a git repo keeps its own box. The per-message label still comes from the directory the session actually works in, so worktrees are told apart by their branch:

- 14:32 main#a1b2c3d4: pushed the release branch
- 14:33 feat/entry-field-visibility#9f8e7d6c [lock]: rebasing, hold off on app/

Anything wider than project is tagged with its scope and the project it came from, since the reader is sitting in a different repo:

- 14:32 master#a1b2c3d4: editing dynamics.rs
- 14:33 [group] my-app/feat-auth#9f8e7d6c [lock]: holding :1420
- 14:35 [global] infra-scripts/master#5a4b3c2d: rebooting the NAS

Groups are opt-in and named by you, one per project. The first time a session tries to send at group scope from an ungrouped project, the tool refuses and tells the session to ask you which group the project belongs to (offering the names that already exist); it then calls set_group with your answer. Membership lives machine-wide in ~/.claude/.session-bus/projects.json - nothing about the bus is written into your repo.

you>     tell the other repos the API contract changed
claude>  broadcast(..., scope="group")
         -> refused: no group set for C:/PROGRAMMING/my-app.
            Existing groups: infra. Ask the user, then call set_group.
claude>  This project isn't in a session-bus group yet. "infra" exists -
         join it, or name a new one?
you>     tauri-work
claude>  set_group("tauri-work") -> joined. broadcast resent.

Quick start

  1. Clone the repo:

    git clone https://github.com/exalsch/claude-session-bus.git
    cd claude-session-bus
  2. Smoke-test the primitives (no Claude needed):

    node scripts/selftest.mjs    # expect: selftest OK - { ... }
  3. Install as a local plugin, then enable it:

    /plugin marketplace add /path/to/claude-session-bus    # the directory you just cloned
    /plugin install claude-session-bus@session-bus-dev

    Restart Claude Code so the MCP server + hook load. Verify the broadcast and inbox tools appear, and that the session-bus MCP server is connected.

  4. Use it. From any session:

    • broadcast("holding the dev server on :1420", kind="lock") - this project.

    • broadcast("api contract changed", scope="group") - every project in this project's group (it will ask you to name one the first time).

    • broadcast("rebooting the NAS", scope="global") - every session on the machine.

    • inbox() to re-read peer messages on demand, across all three scopes.

    Receiving sessions see the message prepended to their context the next time you submit a prompt there.

End-to-end test

Open two terminals, both cd into the same project (e.g. ~/projects/your-app):

  1. In session A: ask it to broadcast("test from A").

  2. In session B: submit any prompt. B should report a [session-bus] line from A.

  3. In session A: submit any prompt. A should not see its own message (origin filter).

If A does see its own message, the ppid correlation did not hold (see Risks in the spec) - the fallback is to render own posts tagged "(you)" instead of filtering them.

For the wider scopes, open a third terminal in a different project:

  1. In session C: ask it to broadcast("test from C", scope="global"). A and B should see it on their next prompt, tagged [global] <project>/<branch>.

  2. In session C: ask it to broadcast at scope="group". It should stop and ask which group the project belongs to. Answer, then do the same in A. Once both projects name the same group, group messages flow between them and no further asking happens.

Config

  • SESSION_BUS_BACKFILL_MIN (default 0) - minutes of backlog a session sees on its first prompt. 0 = forward-only (a new session only sees what arrives after it joins). Applies per scope.

  • SESSION_BUS_RECEIVE (default project,group,global) - which scopes reach this session. Set it to project to go back to v0.1 behaviour, or project,group to stay out of the machine-wide feed.

  • SESSION_BUS_HOME (default CLAUDE_CONFIG_DIR, else ~/.claude) - where the group and global mailboxes and the group registry live. Mainly for tests and for running two isolated buses side by side.

  • Retention (MAX_LINES, MAX_AGE_MS) lives in lib/bus.mjs.

Status & next steps

v0.2 adds the three scopes above. v0.1 behaviour is the default: a broadcast with no scope still goes to the project mailbox and nothing else, and existing project mailboxes and cursors keep working untouched.

The v0.1 headline risk - ppid correlation on Windows - was tested and did not hold: Claude Code spawns the UserPromptSubmit hook through a shell, so the hook's parent PID is a transient shell, never the directly-spawned MCP server's PID. Sessions correlate on the session id instead.

CLAUDE_CODE_SESSION_ID alone turned out to be insufficient (fixed in v0.2.1). That variable is frozen when a process is spawned, and the MCP server is long-lived: across a resume or a plugin reload its copy goes stale, and a stale sender id means a session no longer recognises its own posts and receives them back. Observed live - a server stamping 292becbc… while its own hook half was c0430fa1…, with 292becbc… belonging to no session at all. originId() now reads Claude Code's live process registry at <claude-home>/sessions/<pid>.json first, keyed by the caller's parent pid: the MCP server's parent is claude.exe, so it gets the current id, while the hook's parent is a transient shell with no entry and keeps resolving by env var, which for the hook is accurate. Both halves land on the same id, and the env var remains the fallback. Covered by scripts/test-origin.mjs (unit) and scripts/test-drain.mjs (integration); npm test runs the full suite.

Cost of the receive hook

The hook runs on every prompt and blocks it, under a 10s timeout, so its budget is the one number worth watching. v0.2.3 removed the last subprocess from that path.

Both halves used to shell out to git - rev-parse --git-common-dir to find the repo family, rev-parse --abbrev-ref HEAD for the branch label. On Windows a process launch is scanned, so those metadata lookups were priced like program launches. git writes all of it to disk anyway (.git, the gitdir: pointer in a linked worktree, and <gitdir>/commondir), so the bus reads the files directly. Measured on the dev machine:

before

after

resolve the project root

229ms

0.5ms

in-process work, whole hook

249ms

13ms

end to end, one hook

318ms

152ms

end to end, 16 hooks at once

910ms

418ms

What remains is node's own startup plus ~13ms of file IO, and nothing in the receive path spawns a process - so the bus now also works where git is not on PATH at all (scripts/test-nogit.mjs proves it by resolving with PATH stripped, checked against real git's answers for a checkout, a subdirectory, a linked worktree, a bare repo and a non-repo).

A timeout no longer costs you messages. Claude Code discards the output of a hook that overruns, and the hook used to advance its cursors before writing that output - so each timeout silently ate the peer messages it had just consumed. Cursors are now committed only after the payload is delivered, making the worst case a message shown twice rather than one lost.

Still out of scope: multiple groups per project, directed project-to-project addressing, group discovery beyond the registry, and any reply path.

This is dev tooling - it must never live inside a product repo.

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables real-time messaging between Claude Code instances, allowing agents to send, receive, and reply to messages instantly via file-based communication with auto-notification.
    2
    MIT
  • A
    license
    Not graded
    quality
    F
    maintenance
    Enables file-based agent-to-agent communication between Claude Code instances on the same machine, using MCP channels and plain JSON files.
    7
    14
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    Enables communication between Claude Code sessions in iTerm2 panes, primarily for notifying other sessions when a PR is merged to main so they can pull latest changes.
    6
    MIT

View all related MCP servers

Related MCP Connectors

  • StremAI MCP: shared memory for AI coding agents. Connected agents can recall. OAuth + local stdio.

  • The team layer for AI coding agents: shared contracts, collision alerts, E2EE sessions.

  • Read and write your Fresh Jots notes from Claude, Cursor, and any MCP client.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/exalsch/claude-session-bus'

If you have feedback or need assistance with the MCP directory API, please join our Discord server