Two terminals open. One session has just renamed a column. The other is three files deep in code that reads the old name.
You normally find out when the tests go red. Then you alt-tab and re-explain, in your own words, something you already explained once.
Since v2.1.224 the first session can tell the second one itself.
Peer sessions (1):
chaos-mcp-e7 [15619a] · interactive · busy · started 3h agoThat is this machine. Two Claude Code sessions, each one aware the other is there.
What cross-session messaging actually is
Text. That is the entire payload.
One session's Claude writes a sentence, another session's Claude reads it. No conversation history, no files, no context. The receiver learns exactly what the sender chose to type and nothing else.
Two tools do the work. ListAgents finds the sessions Claude can reach, SendMessage delivers to one of them by name. Both belong to Claude: you describe the outcome, Claude sends.
Requirements first, because most of "it doesn't work" is one of these:
- v2.1.224 or later. Check with
claude --version. - macOS or Linux, WSL 2 included. Native Windows doesn't have it.
- Not on Bedrock, Claude Platform on AWS, Google Cloud's Agent Platform, or Microsoft Foundry.
- Feature-flag evaluation left switched on.
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC,DISABLE_TELEMETRY,DO_NOT_TRACKandDISABLE_GROWTHBOOKeach turn off the flag evaluation this feature rides on. Set any of them, from your shell or a settingsenvmap, and messaging is off with no notice.
The one-second check is /list-agents, also spelled /peers. If the command isn't recognised, the session doesn't have the feature. If it works, messaging is on and anything that goes wrong afterwards is a narrower problem.
Sending a message
You never address anything by hand. Describe the outcome:
Ask the session in my other terminal whether the migration finishedOr leave the wording to Claude entirely:
Tell the session working on the payments API what we just changedClaude picks the target, writes the text, and sends. What lands on the other side is one line:
Schema migration finished: the new column is tenant_id, and rebasing on main is safe now.It arrives with the sender's name and a reply address, queued if the receiving Claude is mid-turn and starting a fresh turn if that session is idle. Once read, it collapses to a one-line Message from row that Ctrl+O expands.
Claude also sends unprompted, when it notices it has done something another session is standing on.
Names are the address
Every session answers to a name.
claude --name paymentsOr /rename mid-session. Set nothing and Claude Code derives one from the folder name plus a short hash. The session writing this post registered itself as tim-vitepress-38.
Two sessions in different directories can end up with the same name. /list-agents prints each local session's working directory to tell them apart. Claude's own listing carries the short identifier you can see in the output at the top of this post, and it addresses by that identifier when names collide.
The registry file and the inbox socket
Every case of two sessions failing to see each other comes down to this.
Each session writes a small JSON file and binds a Unix socket. This is this session's registration, verbatim:
{
"pid": 1318539,
"sessionId": "56593ac1-515e-4e33-b078-0810410f98b0",
"cwd": "/root/tim-vitepress",
"version": "2.1.226",
"peerProtocol": 1,
"kind": "interactive",
"messagingSocketPath": "/run/user/0/cc-socks/1318539.sock",
"name": "tim-vitepress-38",
"nameSource": "derived",
"status": "busy"
}Discovery is filesystem discovery. Two sessions can reach each other exactly when they can see the same files. A session inside a container and a session on the host never can, whatever you configure, because the container has its own filesystem. Two sessions inside the same container can, including on a self-hosted runner.
The socket is per-session and restricted to your operating-system user:
srw------- 1 root root 0 Aug 9 15:14 1318539.sockOn a shared box, another user's sessions cannot reach it.
You can find your own in two places: the Peer address row in /status, prefixed with uds:, and the CLAUDE_CODE_MESSAGING_SOCKET environment variable. Claude Code exports it to every hook and Bash command before anything runs, SessionStart included, and each session exports its own rather than one inherited from a parent.
That variable is the part worth building on. It means a hook or a plain shell script can post into the session it belongs to. A long test run finishes and drops a line into the terminal that is waiting for it. A deploy wrapper reports back without you watching the log.
Read the platform wrinkle before you rely on it. When no crossSessionInbound value applies, Claude Code delivers a message it can verify came from the session's own child process. On Linux, WSL 2 included, it can verify that even after the child has exited. On macOS only while the posting process is still running, and in a container where Claude Code runs as PID 1, not at all.
Whenever it can't verify, the message is treated like any other that asserts no permission class, so a session bypassing permission prompts will hold it for your approval.
A message is not consent
Here is the part that got the design attention, and the part every "my agents can talk now" post is going to skip.
Claude Code tells the receiving session that the message came from another Claude session, not from you. That distinction is enforced:
- It cannot approve anything. A message from another session never counts as your consent, so it cannot answer a pending permission prompt on your behalf.
- It cannot change configuration. The receiving Claude is instructed never to touch permission settings,
CLAUDE.mdor anything else configuration-shaped because another session asked. - Commands arrive as text. A
/compactin the message body is eight characters. Claude Code never runs it. - Permission prompts still fire. Acting on the message needs the same approvals as work you asked for yourself.
The sending side is fenced too. Claude is told never to ask another session for something that was denied or blocked in its own session, or that its own permission rules would block, and to route that work back to you instead.
Read those together and the threat model is obvious: a session with fewer restrictions being used as a laundry service for a session with more. The feature is built on the assumption that one of your own agents will eventually try exactly that.
The same release makes the point twice, in a line most write-ups skipped. From the 2.1.224 changelog:
Fixed sandbox filesystem deny entries written with a trailing slash
(e.g. denyRead: "~/.aws/") being silently bypassable on Linux and macOSSo every denyRead or denyWrite rule you wrote with a trailing slash was decoration. No error, no warning, and the credentials you thought were fenced off were readable the whole time. If you sandbox your sessions and any of those paths end in a slash, that alone is the upgrade argument. Go and look at yours before you read the rest of this.
Delivered, held, or refused
Between two ordinary interactive sessions on default settings, a message arrives. Other configurations hold it or drop it, and the difference matters the moment you script anything.
Every arriving message is checked against the receiver's inbound controls and ends in one of three states:
| Outcome | What happens |
|---|---|
| Delivered | Claude Code hands the text to the receiving Claude |
| Held | Set aside undelivered, until you approve it or the settings change |
| Refused | Dropped without delivery |
crossSessionInbound picks between them: accept, hold or refuse.
When no value applies, the default decides per message, and it decides by permission mode. Sessions fall into two classes: those that bypass permission prompts, and everything else. Plan mode counts as bypassing in sessions where bypass permissions are available. auto, acceptEdits and dontAsk all count as prompting.
- The receiver prompts for permissions: messages are delivered, except one from a sender that identifies as bypassing, which is held for you.
- The receiver bypasses permission prompts: messages are held for you, except one from a sender that also bypasses.
The --dangerously-skip-permissions session gets quarantined at both ends, which is the right instinct.
A held message opens an approval dialog in the receiving session showing the sender and a preview. Approve delivers that one message, deny drops it, and ignoring it drops it once dialogExpiry passes, five minutes by default. Change the session's permission class while messages are held and the rules are re-applied, so held messages can still be released later. Flip to refuse while messages are held and every one of them is dropped, with a denial reported to each sender it can reach.
Held messages cap at 100, and accepted-but-unread ones at 50 per session. Repeats from one sender are rate-limited, and identical repeats inside a short window are dropped outright. That is why two sessions cannot get stuck talking to each other forever.
The precedence is deliberately one-way
Most Claude Code settings let a project file layer over your user settings. Here the layering runs one way only:
Claude Code reads managed settings first, then the
--settingsflag, then user settings, and applies the first value found; a value in project or local settings applies only when it's stricter, on theaccept<hold<refuseladder, than the value those trusted sources give.
A checked-in project file can tighten and can never loosen. When none of the trusted sources sets a value, a project or local hold or refuse still applies, replacing the per-message default.
Cross-machine isolation has the same shape:
{
"isolatePeerMachines": true
}A true from any scope applies, so a project file can switch the requirement on and nothing can switch it back off. With it set, Claude asks for your approval before any reply leaves the machine, even in bypassPermissions mode. Same-machine messages don't prompt.
Turning it off
Receiving and sending are separate controls, so you can close whichever direction you need.
Stop receiving:
{
"crossSessionInbound": "refuse"
}Stop sending and listing, with deny rules that take the bare tool name and no specifier:
{
"permissions": {
"deny": ["SendMessage", "ListAgents"]
}
}Both together, in managed settings, is the organisation-wide off switch:
{
"permissions": {
"deny": ["SendMessage", "ListAgents"]
},
"crossSessionInbound": "refuse"
}Two things to know before you ship that. Denying SendMessage also removes messaging to subagents and agent-team teammates, since one tool serves all three. And a refusing session looks entirely normal, in its own /status and in other sessions' listings, so the only way to confirm it is to read the configuration.
Other machines, where the docs are already out of date
Where the other session runs decides how the message travels.
| Where it runs | How the message travels |
|---|---|
| This machine | Per-session socket, never through Anthropic servers |
| Another of your machines | Anthropic servers, over that machine's Remote Control link |
| Claude Code on the web | Anthropic servers, straight to the cloud session |
The documentation page says everything beyond this machine is reply-only: Claude here can answer a message that arrived from your laptop, and cannot start the conversation. That was true for exactly one release. The 2.1.225 changelog:
SendMessage can now start a conversation with your Remote Control sessions on
other machines by name (ListAgents shows them as name [ref]), instead of only
replying after they message you firstOn 2.1.226 the ListAgents tool describes its own reach the same way, with no reply-only clause anywhere in it. The docs page still carries the 2.1.224 rule.
Worth knowing which of the two you are reading. The reply-only limit is the kind of constraint you would design a workflow around, and it lasted one release. Remote Control still has to be connected here for those sessions to appear at all, and web sessions are the corner the changelog entry doesn't name.
One trap survives regardless. Reply to a session beyond this machine while the replying session isn't connected to Remote Control and the reply still goes through, as a direct request to Anthropic servers, but it arrives with no reply address. The other side cannot answer it. Claude is told as much when it sends, which is more warning than most tools give you.
Unattended workers
A claude -p session binds a socket like any other and shows up in the listing, so a long migration or test run can report back to the session you're watching.
What it cannot do is show an approval dialog. A held message in a -p session stays held, and only a later mode or settings change releases it. To let an unattended worker take messages, say so when you start it:
claude -p "run the migration" --settings '{"crossSessionInbound":"accept"}'An accept in your user settings works too, and applies to every session you run, which is usually more than you meant. Bare mode, the stripped-down headless start that skips most of the session setup, binds no socket at all, so those sessions are invisible and unreachable.
What this is not
The failure mode is treating a message as shared context. It is one sentence, chosen by the sender, arriving with nothing attached.
When what you actually want is the other session's context, the feature you want is a different one:
| What you want | Use |
|---|---|
| Continue one conversation elsewhere | Resume the session |
| Sessions coordinating on one repository | Messaging, with worktrees doing the file half |
| A supervised team Claude spawns | Agent teams |
| Watch and steer many sessions | Agent view |
| Push CI or chat events into a session | Channels |
Messaging covers the narrow case: a session you steer learns one thing another session you steer needs, mid-task, right now.
Every other multi-agent story this year has been about giving agents more reach. This one shipped a channel and then spent most of its rules on what cannot travel down it. A message carries information and no authority, which is precisely what you want from the terminal in the other window.
Back to that renamed column. The second session gets one sentence about tenant_id, and then does what it would have done if you had typed the sentence yourself: asks you before it touches anything.