A message router for AI coding agent sessions on the same machine to talk to each other by name, over a Unix domain socket. Store-and-forward: a message to a name that isn't connected yet waits in memory until it is. Any agent that can run a shell command and read a CLAUDE.md/AGENTS.md-style instruction file can use it — Claude Code, Codex, or otherwise; nothing about the protocol or CLI is Claude-specific past the default socket path.
Not part of mountOS — this is infrastructure for agent session-to-session coordination, kept in mountos-tools alongside the other external tools.
No authentication, no encryption, no per-agent identity check: whoever can
reach the socket can register as any name, read what's queued for it, and
send as anyone. The socket file is created mode 0600 (owner-only), so on
a normal single-user machine that means "anything running as you can use
it" — which is the intended scope. This is for local sessions on one
machine, not for exposing over a network or sharing across users/hosts. Do
not put anything in a message you wouldn't want another local process
reading, and don't proxy the socket to a network port.
Requires a Go 1.24+ toolchain.
cd sbus
go install ./cmd/sbus
This installs the sbus binary to $(go env GOPATH)/bin (~/go/bin by
default). Confirm that directory is on $PATH:
command -v sbus
If that prints nothing, ~/go/bin isn't on $PATH yet — add it in your
shell rc (~/.zshrc, ~/.bashrc, ...):
export PATH="$(go env GOPATH)/bin:$PATH"
then open a new shell (or source the rc file) and re-run command -v sbus. Re-run go install ./cmd/sbus after editing the source to pick up
changes — there's no separate build step for agents to run.
An agent won't know sbus exists unless something tells it. Drop a section
like this into whatever instruction file your agent loads — CLAUDE.md
(~/.claude/CLAUDE.md for every project, or a project-level CLAUDE.md
for one repo), AGENTS.md, or equivalent:
## Session bus (sbus)
To message another AI agent session on this machine, use the `sbus` CLI
(on PATH; setup and full protocol: <path-to-this-repo>/README.md). Never
start a server yourself — `send`/`recv`/`listen`/`ack` all auto-start the
hub on first use if it isn't already running.
- `sbus send --as <my-name> --to <name>[,<name>...] [--ack] "text"`
- `sbus recv --as <my-name> [--wait 10s]`
- `sbus listen --as <my-name>` never exits on its own — run it with
whatever your harness gives you for a background task that reports
output as it streams, not one that only reports back at process exit.
An exit-only background runner will never notify you: `listen` has no
exit to wait for.
- `--to "*"` broadcasts to every currently-listening session; `--to
"prefix:*"` scopes it to listening names starting with `prefix:`.
- Pick `<my-name>` to be identifiable (project + role), e.g.
`mountos-servers:blockserv-migration`.Put it in the global file if you want every session on the machine to know
about sbus regardless of which repo it's working in; put it in a
project-level file to scope it to one project's sessions. Two agents can
only reach each other if they're pointed at the same socket (see below), so
if your agents live in different sandboxes/containers, set $SBUS_SOCK to
a path both can actually see instead of relying on the default.
send, recv, listen, and ack all self-bootstrap: the first one that
finds nobody listening on the socket spawns sbus serve detached and
retries. There is no install step, service file, or manual start/stop.
Logs from an auto-spawned hub land at ~/.claude/sbus.sock.log.
Any number of connections can register listen/recv --wait under the
same name at once — registering never evicts an existing listener. A
message reaching that name (a plain send, a receipt, or a matching
broadcast) is delivered to every one of its currently-live listeners, not
just one. This makes a standing listen and a one-off recv --wait under
the same name safe to run at the same time — the one-off doesn't disturb
the standing listener — and it makes restarting a session's listener under
the same name safe too: the old connection just keeps running alongside
the new one until it's stopped or its process ends on its own.
sbus send --as NAME --to NAME[,NAME...] [--ttl DURATION] [--ack] [--reply-to ID] BODY
sbus recv --as NAME [--wait DURATION]
sbus listen --as NAME
sbus ack --as NAME --id ID
sendis fire-and-forget: it returns as soon as the hub has accepted or rejected the message for each target, it does not wait for the target to read it.--totakes one or more names, comma-separated. A trailing*is a broadcast pattern instead of a name:"*"reaches every session currently runninglisten;"prefix:*"reaches only listening names starting withprefix:. Broadcasts are never queued — a session that isn't listening right now just doesn't see it. Use it for presence/announcement traffic ("I'm touching this folder"), not for anything a session must eventually see.recvdrains whatever's queued right now and returns. Add--wait 30sand, if nothing was queued, it blocks up to that long and returns as soon as one message (or receipt) arrives — it does not sit there for the full 30s once it has something to show you.listenisrecvwith no time limit — it blocks and streams messages forever, so it never exits on its own. That makes a plain "run this in the background" primitive the wrong tool if that primitive only reports back when the process exits (a common default): it will sit there silently and never notify you, sincelistengives it no exit to catch. Use whatever your harness offers for a background task whose output streams back live, line by line, while it's still running.- If your harness only notifies on process exit and has nothing like that,
relaunching
recv --wait Nin a loop is the fallback: sincerecv --waitreturns on the first message rather than streaming likelisten, each loop iteration exits (and notifies you) as soon as something arrives, or after at mostNif nothing does — it just costs a relaunch per message instead of one long-lived listener. --ackonsendasks for receipts in your own mailbox: adeliveredreceipt the moment the message leaves the queue (read viarecv/listen, automatic), and anackedreceipt if the recipient later runssbus ack --id <the message's id>once it's actually handled the message, not just seen it.--reply-to <id>onsendthreads a reply to an earlier message: the receiver seesid(the new message's own id, for further ack/reply) andreply_to(the id it's answering) on delivery. Purely informational — the hub doesn't validate that the referenced id ever existed.- Every delivered message prints its own id, sender, sent time, age, and
(if set) what it's a reply to
(
[2026-08-29T05:38:04+05:30 +4s] id=2 sess-B (re: 1): body), so the receiver can tell whether it's still relevant and what to reply to. - Socket path defaults to
~/.claude/sbus.sock; override with--sockor$SBUS_SOCK.
# session B: run under a streaming-aware background watcher (not a plain
# fire-and-forget background shell — that only reports at exit, and
# listen never exits):
sbus listen --as sess-B
# session A, from anywhere on the machine:
sbus send --as sess-A --to sess-B --ack "restarting the blockserv pair, hold off on writes"
# session B (or, if it wasn't listening, the next time it checks):
sbus recv --as sess-B --wait 5s
# session B, once it's actually acted on it:
sbus ack --as sess-B --id <id from the recv output>
See the doc comment at the top of internal/proto/proto.go for the full
NDJSON envelope spec. Delivery is at-most-once — the hub never retries on
its own; --ack/ack exist so a sender can find out what happened instead.
Queues are capped (100 messages, 24h TTL per mailbox by default) and swept
periodically; a message can request a shorter TTL but never a longer one.