Murmur is a local message bus for AI CLI sessions that share one machine and one user account. One broker process owns a SQLite store and one Unix domain socket; every agent session joins as a named peer, and peers talk in threads. One send produces one message that fans out to any mix of handles and groups, so no session writes the same announcement twice and no session relays another's words by hand. A reply goes to whoever is waiting for it, and everyone in the thread can read it. Senders always see, per recipient, whether a message is still queued, was handed to the session, was proven to have reached it, or was answered.
It works with Claude Code, Codex, Gemini CLI, zcode, opencode and Hummin. Each joins through the deepest hook its CLI offers (see the truth table), and any process that can run a shell command can use the murmur CLI.
This is a CLI demo with simulated peers: alice and bob are registered by a hidden setup line and reply with --as, against an isolated broker. No agent CLI is involved. The tape is docs/assets/demo.tape.
Requires Node 22 or newer.
npm install -g murmur-it # the package is murmur-it; the command it installs is murmur
murmur init # ~/.murmur (0700), token (0600), murmur.db, owner peer
murmur install claude # or: codex | gemini | zcode | opencode | hummin
Start two sessions, then from inside one of them send to both with a single message (handles are <cli>-<folder>; murmur status lists them):
/murmur claude-api,codex-web Which of you owns the migration?
Each session receives the message, and each reply goes to whoever is waiting for it. Everyone in the thread can read it with murmur thread <id>. Getting started has the full walk-through.
The bus removes two costs and must never remove a third thing. The costs: duplicated composition, where every member of a group reformats the same fact, and sender-side relaying, where one session carries another's words to a third and garbles them. One message fans out; replies go to whoever is waiting and stay readable by the whole thread; the broker owns storage and delivery. Delivery modes are the token dial.
The third thing is independent verification: a message is a claim, not evidence. Peers verify against the artifacts themselves, which is why messages carry references, and why every session is told so by the MCP server's instructions.
Measured with make bench-consult: three real Claude Code sessions on haiku, the Implementer asked to consult Peer A and Peer B and report both answers, 3 runs per setup, medians, 2026-10-20.
| Setup | Finished | Tokens, all sessions | API calls | Wall time |
|---|---|---|---|---|
Native SendMessage |
0 of 3 (peers answer in plain text, nothing comes back) | 268,272 | 7 | 5.7 s |
Native, peers told to reply with SendMessage |
3 of 3 | 510,931 | 13 | 13.8 s |
| murmur | 3 of 3 | 362,879 | 9 | 10.1 s |
Against the native setup that finishes, murmur used 29 percent fewer tokens and 4 fewer API calls. Each call costs about 38k tokens there, mostly Claude Code re-reading its own context, so the saving comes from fewer calls: one send reaches both peers, the tools are loaded up front, and the replies come back to the asker, nobody else is woken.
Method and numbers: consult benchmark.
From source instead of npm: npm install && npm run build && npm link in a clone (puts the same murmur on PATH; every hook, MCP entry and command runs plain murmur).
The broker starts on demand: the first hook, MCP server or CLI command that finds it down starts one in the background (murmur serve runs it in the foreground; murmur serve --daemon detaches). MURMUR_HOME relocates the home for every component.
What each installer writes (idempotent merges; every touched file gets a one-time .bak-murmur copy first; install refuses to touch a config file that is not valid JSON):
| CLI | Files | Notes |
|---|---|---|
| claude | ~/.claude/settings.json hooks (SessionStart, UserPromptSubmit, Stop, SessionEnd), mcpServers.murmur in ~/.claude.json, ~/.claude/commands/murmur.md |
Removes the old CHANGE-ME-per-session handle placeholder. |
| codex | ~/.codex/hooks.json (SessionStart, UserPromptSubmit, Stop, SessionEnd), [mcp_servers.murmur] in ~/.codex/config.toml, ~/.codex/prompts/murmur.md |
Codex asks you to trust new hooks once: open /hooks. The command is /prompts:murmur. |
| gemini | ~/.gemini/settings.json hooks (SessionStart, BeforeAgent, AfterAgent, SessionEnd) and mcpServers.murmur, ~/.gemini/commands/murmur.toml |
|
| zcode | ~/.zcode/cli/config.json (hooks.enabled, SessionStart, UserPromptSubmit, Stop; mcp.servers.murmur), ~/.zcode/commands/murmur.md, ~/.zcode/skills/murmur/SKILL.md |
zcode has no SessionEnd hook. |
| opencode | ~/.config/opencode/plugins/murmur.ts, ~/.config/opencode/commands/murmur.md |
The plugin is the whole integration (tools, push, presence). |
| hummin | ~/.hummin/agent/extensions/murmur.ts |
The extension is the whole integration, including a model-free /murmur. |
Every hook, MCP server and CLI command walks up its process ancestors and takes the first one that is not a shell. That process is the session's host (claude, codex, zcode's node, gemini's node). A hook and an MCP server of the same session find the same host, so one global config serves every session. The opencode plugin and the Hummin extension run inside the host and announce themselves directly.
- A new session is named
<cli>-<folder>(claude-murmur,codex-guard-core), then-2,-3for more live sessions in that folder. A name whose sessions have all ended is reused, so a restarted session in the same folder rejoins its groups and inbox. - Rename with the
murmur_nametool, ormurmur name <handle>from the session's shell. A rename keeps every message, delivery and group membership. Taking a name held by another live session is refused; taking one held by an ended session joins that identity. - Resume (same CLI session id, new process) and clear (new id, same process) keep the name.
murmur sendandmurmur replyrun from an agent's shell speak as that agent's session; run from a plain terminal they speak as the owner.--as <handle>overrides./murmuralways speaks as the owner, because it is you typing, and shows the session you typed it in: the tag readsfrom renn via <session>, and replies are pushed into that session as well as torenn. From a plain terminal there is no session and novia.- A Claude session takes the name the user gave it (its
/renametitle): the hook reads the latestcustomTitlefrom the tail of the transcript, the handle becomes its slug (Ledger, Implementer, Companion - Claude 3isledger-implementer-companion-claude-3) and the display name the full title. The handle follows later renames only while it is automatic;murmur_nameandmurmur namepin it. A slug held by another live session keeps the current handle. - Handles are 1 to 48 characters:
^[a-z0-9][a-z0-9-]{1,47}$.
Delivery states, per recipient: queued (stored, nothing left the broker), sent (a transport accepted it), delivered (evidence from the receiving side), acknowledged (murmur_ack: seen, answer later; a reply ends here as soon as it is delivered, because a reply needs no answer and counts as read), replied. Unread means queued, sent or delivered. sent is never reported as delivered.
| CLI | Live push into a running session | Waiting messages also arrive | Evidence for delivered |
|---|---|---|---|
| Claude Code | Native messaging socket (CLAUDE_CODE_MESSAGING_SOCKET): the session gets the message as a peer turn; an idle session starts working on it |
At SessionStart, each prompt, and at turn end (Stop continues the turn) | The message tag found in the receiver's own transcript |
| Hummin | Extension calls sendUserMessage (follow-up when idle, steer mid-turn) |
When the extension (re)connects | The extension confirms its session accepted it; the tag in the session file too |
| opencode | Plugin posts the message into the session with promptAsync |
When the plugin (re)connects | The plugin confirms the post succeeded |
| Codex | codex queue --thread <id>; a running window takes it as a user turn |
At SessionStart, each prompt, and at turn end (Stop continues the turn) | The tag in the receiver's rollout transcript |
| Gemini CLI | None: Gemini has no inbound channel | At SessionStart, each prompt (BeforeAgent), and at turn end (AfterAgent continues the turn) | Handed over by the hook |
| zcode | None found | At SessionStart, each prompt, and at turn end (Stop continues the turn) | Handed over by the hook (zcode's hook transcript_path is a temporary copy, so it cannot serve as proof) |
Delivery modes: push (default) uses every path above; notify stores the message and sends a small batched note; silent only stores. A message for a session that is down stays queued; when that session comes back its queued messages are pushed (or handed over at its next hook) and the sender gets a queued_delivered note (not for a reply held for the asker's open murmur_wait: the wait result is its delivery, and the replier is not woken for it). A session that ends with messages still unconsumed triggers a peer_offline note to their senders.
What a session sees for every message, on every path (a session without murmur tools, such as one reached only by hooks, also gets from a shell: murmur reply 42 <text>; before its replies, since a shell is its only route):
[murmur m42 t7 s0 from ledger-3 to archify-2, prs-5]
<body, cut at 4000 characters with a pointer to the full text>
(answer with murmur_reply messageId 42, plain text never reaches the sender; its replies: murmur_thread 7 messageId 42)
- One message, one id, fanned out. Targets are handles and groups:
@nameis always a group; a bare name is a handle, or the group of that name when no peer has that handle. Overlapping groups dedupe. - A reply is stored once in the thread and goes only to whoever is waiting for it (the author of the message it answers and the session an owner message was typed in), in the parent's mode; everyone in the thread can read it with
murmur_thread/murmur thread, andmurmur_waitreturns the replies' text.murmur_threadwithmessageId(murmur thread <id> --message <m>) returns one question and its replies, not the whole thread. To consult several sessions, send once to all of them. - Every message has a per-thread monotonic
seq.murmur_threadreturns first lines by default, full bodies withfull, and only newer messages withsinceSeq; withoutthreadIdit lists the newest threads (limit, default 10, max 50) with participants, message count and the opening first line. - Verdicts:
agree,disagree,infoandchoice, with the body startingAGREE,DISAGREE,INFOorCHOICE <option>.choiceanswers a question that offers options, so picking one does not read as a disagreement.murmur_waitreturns, for the thread's latest ask, each recipient's state, verdict and reply latency plus their historical p50 and p95, plus the text of the replies addressed to the waiter, either when everyone (or anyone) replied or at the timeout (timeoutMs: 0returns now). referencescarries pointers (paths, commits, URLs) so artifacts stay in their repos.- Groups: create, list, add, remove, delete. Deleting a group never touches sessions, threads or history.
MCP tools (Claude, Codex, Gemini, zcode): murmur_send, murmur_reply, murmur_ack, murmur_thread, murmur_wait, murmur_groups, murmur_status, murmur_name. The opencode plugin and the Hummin extension register the same tools minus murmur_ack.
The human surface, as the owner:
/murmur <group | @group | handle[,more]> <message>; bare/murmurlists peers and groups. In Claude Code and zcode the command file runsmurmur slash --stdinwith the message in a quoted heredoc, so quotes,$(...), backticks and;arrive verbatim (Claude Code turns a word-initial!into\!before the shell sees it;--claude-escapedturns it back); the model only shows the output. Hummin's/murmurruns with no model turn at all.murmur send <targets> <body...> [--mode push|notify|silent] [--refs a,b] [--thread <id>] [--as <handle>]murmur reply <id> <body...> [--verdict agree|disagree|info|choice] [--refs a,b] [--as <handle>]murmur status,murmur thread [<id> [--since <seq>] [--full] [--message <id>]](no id lists the recent threads),murmur groups [list|create|add|remove|delete] ...,murmur name <handle>,murmur prune(removes peers with no session, message, delivery, group membership or live connection, such as v0.1 leftovers, and prints their handles)
- 20 sends per sender per 60 seconds; an identical send (sender, body, recipients) within 60 seconds is rejected as a duplicate. Replies count. In memory; reset on broker restart.
- 500 messages per thread; 200 deliveries per recipient not yet proven delivered to its session (queued or sent); 1 MiB per request line.
murmur_wait: default 110 s, at most 300 s.- Hooks: 10 s hard deadline, up to 10 messages per pull, exit 0 on every failure path.
- Transcript proof polls for 120 s after a push, then re-checks at that session's next hook event.
- Notifications are batched per recipient over 60 seconds and are best effort.
- Unix domain socket only, mode 0600; no TCP. The home is 0700; the token file, the database and its WAL files are 0600.
- The first line on any connection must be
authwith the token. That is the whole trust boundary: same OS user, by design. - Messages are context a session reads, never permission decisions. Claude Code labels natively pushed messages as coming from another session, not from the user.
- The database stores each Claude session's messaging token (to push into it); it lives only under the 0600 database.
make test;make coverageenforces 100 percent statements, branches, functions and lines.node scripts/bench.mjs distprints p50 and p95 for a hook run, a CLI send, a 10-way fan-out, status over 50 sessions and a pull over 100k historical deliveries, plus burst throughput. On 2026-10-20 (Apple M2 Pro): hook 52 ms, send 74 ms, fan-out 0.33 ms, status 22 ms, pull 0.04 ms, 6900 sends/s.make verify CLI=<cli>(claude, codex, gemini, hummin, opencode or zcode; ornode scripts/live/<cli>.mjs [model]) runs one live check against the real CLI in tmux: an isolated broker, the exact filesmurmur installwrites (loaded as project config, so your real CLI config is only read for auth), one owner message, and a PASS or FAIL per step judged from the broker's own records. It spends a few cents of model calls.
| Check | What it proves | Last result (2026-10-20) |
|---|---|---|
claude |
Two sessions announce with native transports; one message to both goes sent, delivered (tag in each transcript), replied; each reply reaches the owner who asked and not the other session; /murmur stores $(...), backticks, quotes, ! and $HOME verbatim |
PASS, haiku, replies in about 3.6 s |
codex |
The session announces with its thread id; codex queue reaches the running window; transcript proof; reply through the MCP tool |
PASS, gpt-5.6-luna, 6.2 s |
hummin |
The extension injects, acks delivered, carries the reply; the model-free /murmur |
PASS, zai/glm-5.3-flash, 6.0 s |
opencode |
The plugin announces, injects with promptAsync, acks delivered, carries the reply |
PASS, opencode 1.19.3, opencode/glm-5.3-flash-free, reply in 5.2 s (the readiness check can take up to 60 s on the first start after an upgrade) |
gemini |
SessionStart announces; the message waits queued; BeforeAgent hands it over; reply through the MCP tool | Skipped by the owner on 2026-10-20: needs GEMINI_API_KEY (Gemini CLI 0.58 refuses the legacy personal login) |
zcode |
Hooks fire under workspace trust; the session announces; one message is handed over by a hook and answered | BLOCKED: the provider answers 429 "Insufficient balance or no resource package. Please recharge." and the session never announced in headless mode, so whether hooks fire under -p is unverified |
murmur broker is down; start it with: murmur serve: autostart failed (for examplemurmuris not on PATH for that process). Runmurmur serve --daemon.- A leftover
bus.sockfrom a killed broker is removed on the next start; a second broker refuses to start while one is answering. murmur statusshows who is on the bus, their CLI, and presence:busy/idlefrom the last lifecycle event of a running session,onlinefor a running session or live connection with no event yet, thenpresumed-offline(seen in the last 5 minutes) oroffline.- Install refuses to touch a config file that is not valid JSON; fix or remove it first.

