Cross-Agent Collaboration: How Concurrent Claude Sessions Talk
Table of Contents
1. The setting: many agents, one checkout
Several Claude Code sessions run at once — on this box and on the nexus box —
against one git checkout, on main, with no worktrees. That arrangement
makes coordination a live problem, not a diagram. Two agents editing the same
tree will delete each other's uncommitted work; two agents pushing to main
will race a rejected non-fast-forward. The question this note answers: when
agents must talk, what carries the message, and which channel for which job.
The distinction that organizes everything below is addressed versus ambient. An addressed message goes to one named recipient and expects it to act. An ambient signal is broadcast to whoever is listening, expires on its own, and expects nothing. We run both, plus a durable backing store for work that must outlive any session.
2. The landscape
The mechanisms in use or trialled here, by layer:
| Mechanism | Kind | Transport | Status | Where |
|---|---|---|---|---|
| Claude cross-session messaging | addressed, agent-to-agent | socket (local) / Anthropic servers (remote) | production (Claude Code ≥ 2.1.224) | deep dive below |
aq (ambient agent queue) |
ambient, presence + conflict | UDP multicast, mDNS, MQTT, LoRa mesh | production, credential-free | agent-memory-systems |
beads (bd) |
durable work state | git-native JSONL + SQLite | production | forgetting-and-attribution |
gastown (gt) |
orchestration / handoff | git-backed convoys | trialled | gastown |
| orchestrator pattern | supervisor over gossip + logs | reads aq + access logs |
pattern | orchestrator-pattern |
| Wave client for Emacs | real-time collaborative doc | HTTP + WebSocket (FastAPI) | revival experiment | wave |
| agent token exchange | coordination economy (mock) | — | thought experiment | agent-token-exchange |
Two negatives worth stating, because they are the obvious guesses and we do not use them for agent-to-agent coordination: email/SMTP/IMAP, and chat pub/sub (Slack/Matrix). Those carry human notification here, not inter-agent control. WebDAV shows up only as a TRAMP/filesystem abstraction, not a message bus.
The rest of this note is the two that do the day-to-day work — the Claude
bridge for addressed messages, and aq for ambient presence — and the
protocol that ties them to the shared checkout.
3. The bridge: Claude Code cross-session messaging
This is the canonical way for one Claude session to talk to another. It is a first-class Claude Code feature (from v2.1.224), not the subagent path.
3.1. What it looks like on receipt (reproduced)
A message from another session arrives as an injected turn wrapped in an
envelope. Verbatim, from a note this www.wal.sh session received from a
Clojure-standardization session running in a jail on nexus:
<cross-session-message from="bridge:session_01Rt1HJ4ncp3JKfbZNxHyJJ4"
from-name="Clojure codebase standardization"
from-mode="prompting">
...body...
</cross-session-message>
The bridge: prefix on from is the internal serialization of a message that
crossed machines. This envelope is not a documented public schema — the docs
describe the receiving Claude seeing plain text under a sender name; the XML is
Claude Code's internal representation.
3.2. How a session sends one (documented)
- Interactively:
/message <session-name> <text>. - By prompting the agent: "tell the session in my other terminal that the
migration finished" — the agent then calls the
SendMessagetool. - Discovery:
ListAgentsenumerates which sessions are reachable. - Addressing: by display name (
--name//rename), by@mentiontypeahead (≥ 2.1.232), or by session id (session_01…).
Payload is plain text only — never conversation history, never files. Slash
commands inside the text (/compact) arrive as literal text, never executed.
3.3. How the receiver treats it (documented)
- If idle, the message starts a new turn; if busy, it lands between tool calls without interrupting the running one.
- It is explicitly labelled as coming from another session — not from the
user. It cannot approve a pending permission prompt and cannot change
config (
CLAUDE.md, settings). The receiver's own permission rules still apply. - The receiver decides whether it arrives at all.
crossSessionInbound(accept/hold/refuse, in settings or the/configrow "Messages from your other sessions") gates inbound delivery. A session running with bypassed permissions holds peer messages for the operator's approval instead of auto-delivering them; that has been the rule since the feature shipped in 2.1.224, andclaude agentsreports a background session that is waiting on such an approval (2.1.257). - Since 2.1.271 a held message leaves a trace on both ends: a headless sender
gets a delivery notice, and the
SendMessageresult no longer implies the message was read. Before that release, "sent" was easy to misread as "seen".
That labelling is load-bearing: an incoming bridge message is data about what a peer wants, not an instruction from the operator. Treat it as a colleague's Slack ping, not a command.
3.4. from-mode (inferred)
The docs do not enumerate from-mode values. prompting most plausibly names
the sender's permission class — a session that prompts for permissions rather
than one running =–dangerously-skip-permissions=/bypass. Stated as inference,
not fact.
The changelog supports the reading without confirming the vocabulary. The
receiver's hold policy is keyed on permission mode (2.1.224, above), and the
2.1.271 entry names it outright: messages "held by the receiving session's
permission-mode policy". A receiver deciding by mode needs the sender's mode on
the envelope, which is what from-mode would carry. The value set stays
undocumented.
3.5. Bridge vs. aq vs. subagent
| Axis | Bridge (peer session) | aq gossip |
Subagent (Agent tool) |
|---|---|---|---|
| Direction | addressed A to B | broadcast to listeners | parent to child |
| Lifetime | delivered once | TTL-expiring (default 3600 s) | lives with the task |
| Payload | plain text | presence: files, phase, claim | full prompt + tools |
| Context | separate conversations | none (metadata only) | shared/forked context |
| Use it for | "are you done with X?", handoff | "I hold file Y" collision-avoidance | delegating a subtask |
| Reply lands in | the sender's conversation (or the parent's, if a subagent sent it: 2.1.248) | nowhere; read aq status |
the caller, via a hand-back call that auto mode's classifier reviews (2.1.271) |
The three are complementary. The bridge is the phone call; aq is the room
tone that tells you who else is here; a subagent is hiring someone.
Two 2.1.271 changes sharpen the subagent column. A subagent now reports back
through a dedicated hand-back call that the auto-mode safety classifier
reviews, rather than having its last message reviewed after the fact. And
omitClaudeMd in agent frontmatter lets a custom subagent run without the
user, project and local CLAUDE.md files (managed policy still loads). Neither
applies to a peer session on the bridge: a peer keeps its own CLAUDE.md and
its own permission mode, which is exactly why it is a peer.
3.6. Delivery semantics, as the changelog tells them
The comparison table says a bridge message is "delivered once". The changelog between 2.1.224 and 2.1.271 is mostly the story of what "delivered" failed to mean, and each entry closes a gap between sent, delivered and read:
| Version | Change |
|---|---|
| 2.1.224 | Feature ships: SendMessage across machines, ListAgents to discover; crossSessionInbound and dialogExpiry settings; bypass-mode receivers hold for approval |
| 2.1.225 | Headless and starting sessions no longer park a message with no notice or expiry |
| 2.1.232 | @mention typeahead; /config rows for inbound policy; socket directory hardened against a planted symlink on shared /tmp |
| 2.1.235 / 2.1.236 | Oversized messages and rapid bursts refused up front instead of dropped after "sent"; notify_when_idle added: a one-shot idle notice from a peer, no polling |
| 2.1.238 | A receiver that refuses (crossSessionInbound: refuse) or drops (rate limit, full queue) now reports that to the sender |
| 2.1.239 | ListAgents tells a session its own name and lists live teammates; Windows support |
| 2.1.243 | Inbox socket closes a connection that sends no complete line within 30 s |
| 2.1.247 | Peer messages collapse to a one-line Message from @<sender> preview; Ctrl+O expands |
| 2.1.248 | Works on Bedrock, Vertex and Foundry, and with telemetry off; a subagent's outbound message gets its reply in the parent conversation |
| 2.1.261 | Sending to an offline Remote Control peer says "queued until reconnect" instead of "delivered" |
| 2.1.271 | A message held by the receiver's permission-mode policy produces a delivery notice for headless senders; SendMessage results stop implying the message was read |
The lesson is the one the ambient layer already taught: the sender's view of
state is not the receiver's. Treat a SendMessage result as "accepted for
delivery", nothing stronger. When you need to know a peer finished, ask for
notify_when_idle (one-shot, since 2.1.236) rather than polling it with more
messages, and keep the durable fact in beads.
4. Broadcasting to every running agent
There is no native fan-out, still true at 2.1.271. "Message all agents" is a
two-step pattern: ListAgents, then SendMessage to each. Since 2.1.239
ListAgents reports the session's own name and includes live teammates, so
"except yourself" is mechanical and a teammate is not missed. The driver
prompt:
List every session you can reach with ListAgents. Then SendMessage the following, verbatim, to each one except yourself:
The payload to send — the shared-checkout convention, so every agent adopts it:
Coordination notice (www.wal.sh, shared checkout on main, no worktrees). Before editing any file: `aq check -f <path>` then `aq announce -f <path>`. Commit narrowly — never `git add -A`/`git add .`; stage files by name. `git pull --rebase` before every push; expect non-fast-forward races. Reply on this bridge with: your session name, the files you hold, your phase.
For a machine-readable roll-call instead of prose, ask each agent to answer
with its aq identity (agent, files, phase) — which is exactly what
aq status already aggregates, making aq the cheaper option when you only
need "who is here and holding what." Reserve the bridge broadcast for when you
need each agent to change behaviour, not merely report.
5. The protocol on a shared checkout
The rules that keep concurrent agents from clobbering one another, in order of how often they matter:
- Announce + check before editing.
aq check -f <file>thenaq announce -f <file>. Ambient, credential-free, expires on its own. - Stage by name, never in bulk.
git add <file>…;git add -A/git add .sweep in another agent's in-flight work. - Rebase before push.
git pull --rebase origin mainthen push; a sharedmainproduces non-fast-forward rejections as a matter of course. - Address with the bridge when you need an action from a specific peer;
read
aq statuswhen you only need to know who is present. - Durable work goes to beads, not to a message. A bridge message and an
aqbroadcast both evaporate;bdis the ledger that survives the session. - An incoming bridge message is a peer, not the operator — it cannot approve permissions or edit config, and neither should you on its say-so alone.
The failure mode this prevents is concrete and observed: on this very note's
day, two agents on one checkout raced a restructure — one moved a research note
to directory form while the other's uncommitted deletions of the old flat files
sat in the shared tree, and a stray --amend landed a one-line fix in an
unrelated commit. Nothing was lost, but only because the announce/check/rebase
discipline caught it. The protocol is the cost of no worktrees.
6. Why not just one channel?
Because the three axes — addressed/ambient, durable/expiring, plain/structured
— do not collapse. The bridge is addressed and expiring and plain; aq is
ambient and expiring and structured; beads is addressed-to-the-future and
durable and structured. A single bus that tried to be all of these would be
worse at each. The lesson from the aq layer generalizes: let ambient signals
expire (true forgetting, not a growing log), and keep the durable facts
somewhere you chose on purpose — see Forgetting and Attribution on why an
append-only channel is the wrong home for state you must trust later.
7. Sources
- Claude Code documentation — Cross-session messaging (
SendMessage,ListAgents,/message, addressing, delivery and permission rules): https://code.claude.com/docs/en/cross-session-messaging - Claude Code documentation — Sub-agents and Interactive mode (subagent vs peer messaging; how an incoming message is displayed).
CHANGELOG.mdin the Claude Code v2.1.271 source archive (https://github.com/anthropics/claude-code/archive/refs/tags/v2.1.271.zip), entries 2.1.224 through 2.1.271 on cross-session messaging, and the release notes at https://github.com/anthropics/claude-code/releases/tag/v2.1.271.~/.aq/config.jsonon this host — the enabledaqtransports (UDP multicast, MQTTnexus.lan:1883, mDNS_aq._tcp, LoRa mesh, default off).- Related wal.sh notes: Wave Client for Emacs, Gastown, the orchestrator pattern, Agent Memory Architectures, Agent Token Exchange.