BUILD · PHASE 12 · LESSON 77

Cross-Session Messaging

One session can tell another session something without you copy-pasting it —

Type
Build
Prerequisites
Phase 11 (Lessons 71–76), Phase 04 Lesson 30 (Sessions)
Time
~35 minutes

Learning Objectives

  • Send one session a finding by asking a different session to send it
  • Read /list-agents and say which rows are reachable, and why
  • Prove the channel is a per-session socket by closing a session and watching the address stop working
  • Choose between accept, hold, and refuse for inbound messages, and predict what each does
  • Subscribe to an idle notice so a long task tells you when it is done

The Problem

Two sessions were running in two terminals. One changed a database column. The other was halfway through building against the old name.

Nothing told the second session. The user alt-tabbed, retyped the finding in the other terminal, and lost the thread they were holding in their head to do it.

Then they asked the obvious follow-up: "if I close that terminal, can your other session still send it a message?" I said yes. That was wrong.

The docs are specific. The transport on this machine is a per-session socket — a Unix domain socket, or a named pipe on Windows. A closed session has no socket. There is no inbox waiting behind it. A message to a session that is gone is not queued and not retried; there is nothing to retry it to.

That distinction is the whole lesson. Messaging is not mail. It is text pushed at a process that has to still be running.

Related: this repo already has a scar from exactly this mistake. A claim that cross-session workers "survive closing the tab" was made in conversation and corrected later, because the docs say the opposite. Read this one as the correction, written down.


The Concept

Every Claude Code session that has messaging available binds one socket for itself. Other sessions on the same machine deliver into it. The socket is the session's address.

sequenceDiagram
    participant A as Session A
    participant B as Session B (alive)
    participant C as Session C (closed)
    participant D as Anthropic servers

    A->>B: ListAgents → finds B
    A->>B: SendMessage over the local socket
    B-->>A: reply over the local socket
    A--xC: no socket exists. Nothing to deliver to.
    A->>D: only when B is on another machine or in the cloud
Mermaid source — renders as a diagram where mermaid.js is available

Three things follow from "the address is a socket bound to a live process."

One: nothing is stored. A message is delivered and consumed. It is not a transcript and not a queue you can read later. If you want the conversation in another session, you resume the session — that is a different mechanism and Lesson 30 already taught it.

Two: the message is text, not context. Another Claude writes it. Your history, your open files, your uncommitted diff do not travel with it. The receiving session sees a sentence with a sender name and a reply address, and can open any path you mention with its own tools and its own permissions.

Three: it can't act for you. A message is not your consent. It cannot approve a permission prompt, cannot change settings or CLAUDE.md, and a /compact in the body arrives as literal characters. If acting on the message needs a permission the receiving session lacks, you get the same prompt you would get anyway.

Who you can reach

/list-agents (also /peers) prints the machine's reachable set. The first line, when it exists, is this session's own name — that is what other sessions type to reach you.

RowWhat it is
SubagentsAgents inside the current session. Reachable, but messaging them is the same as addressing them directly
TeammatesThis session's own agent-team peers (Phase 11, Lesson 73). These also share a task list
Your other local sessionsOther sessions on this machine — including background ones. Listed only when they bound a socket
Cloud sessionsOnly while this session is connected to Remote Control
Remote Control sessions on other machinesSame condition; labelled Remote Control, and can read offline

A name with a space or punctuation needs quotes: @"release notes". When the typeahead can't tell two sessions apart, Claude Code attaches a short identifier and addresses the message with that instead.

The doorbell

A message arrives as a dim one-line preview, and the preview stays in the transcript. Ctrl+O opens the full text. Claude reads all of it either way — the … is only what you see.

Delivery timing is the part worth internalising: the receiving Claude reads the message between tool calls during an active turn, so a running tool is never interrupted. An idle session starts a new turn to read it.

The reframe: a message is text pushed at a socket that only exists while

the receiving session is alive — so "send it a note" and "send it a note later"

are not the same request, and only one of them is possible.


Build It

You need two sessions for this. Open a second one in another terminal in this repo, or use a background session.

1. Give both sessions names

An unnamed session gets an autogenerated name, which is useless as an address.

/rename schema-work

In the other session:

/rename payments-api

✅ You pass when /list-agents in either session shows both names.

2. Rehearse the reachability question before you need it

In schema-work:

/list-agents

Read every row out loud and say what it is. Then answer this before moving on: which of these rows would disappear if you closed that terminal?

✅ You pass when you can name the row that stops existing when a session exits, and say why in terms of the socket rather than in terms of permission.

3. Send one, without writing it yourself

In schema-work, cause the change, then ask Claude to pass it on:

Explain what we just did to the session working on the payments API.

Claude discovers the target with ListAgents and sends with SendMessage. You never call either tool. Notice that the wording is Claude's, so a vague request gives a vague message.

Then try the direct address:

Let @payments-api know the schema migration finished.

✅ You pass when the preview line appears in the second session and you can say which part of your prompt Claude could have dropped and still succeeded.

4. Prove it is a socket, the hard way

In payments-api, run /status. Find the Peer address row. It is prefixed uds: and it is the path of that session's socket.

Now close that session. In schema-work:

Tell payments-api the schema changed again.

✅ You pass when the send does not arrive, and you can state what did not happen: no queue, no retry, no stored message.

5. Choose what this session does with inbound messages

crossSessionInbound has three values.

/config crossSessionInbound=hold

Careful — the docs say the /config shorthand form is rejected for this key. Set it in the row named Messages from your other sessions, or in a settings file. Send yourself a message from the other session either way.

✅ You pass when the message is held rather than delivered, and you can say what releases it later.

6. Ask to be told when a long task ends

SendMessage can carry notify_when_idle. In the session that is waiting:

Tell me when the schema-work session finishes what it's working on.

Two limits worth knowing before you rely on it: only the Claude in your main conversation can subscribe, only to sessions on this machine, and the subscription is dropped unanswered after 12 hours.

✅ You pass when the notice arrives, and you can name what happens to the subscription if it never does.

7. Post into your own session from a hook

The socket path and a per-session token are exported to hooks and Bash commands as CLAUDE_CODE_MESSAGING_SOCKET and CLAUDE_CODE_MESSAGING_TOKEN. That is how a script running inside a session can put a line back in front of Claude.

{
  printf '%s\n' "{\"type\":\"auth\",\"token\":\"$CLAUDE_CODE_MESSAGING_TOKEN\"}"
  printf '%s\n' "build finished: 3 failures, see /tmp/build.log"
} > "$CLAUDE_CODE_MESSAGING_SOCKET"

One connection, both lines. Two separate > redirects would open two connections, and only the first one would carry the auth line.

Two traps. On native Windows the auth line is required — a connection whose first line is not a valid auth line is closed and delivers nothing. And the connection is dropped if it hasn't sent a complete line within 30 seconds, so capture slow output before you open it.

✅ You pass when the line appears in your own session, and you can say why the token matters more on Windows than on macOS or Linux.

Claude Code ships weekly and this feature is young. The version gates are real:

v2.1.224+ on macOS, Linux, and WSL 2, v2.1.234+ on native Windows, and v2.1.248+

for same-machine messaging on non-Anthropic providers. Verified against

the cross-session messaging page

on 2026-10-15. Check https://code.claude.com/docs/llms.txt before trusting a

number here.


Use It

NeedReach forNote
A session needs a finding from anotherAsk Claude to send itClaude writes the text; be specific or get a vague message
Address a session yourself@name in your promptTypeahead; requires v2.1.232+
Name a session so it can be addressed/rename, or --name at launchAn autogenerated name is not an address
See what's reachable/list-agents (alias /peers)Also shows your own name
Find your socket/status → Peer addressPrefixed uds:
Notice when another session goes idleAsk for it; SendMessage's notify_when_idle12-hour expiry, main conversation only, same machine only
Decide what you acceptcrossSessionInbound: accept / hold / refuseDefault is derived from the two sessions' permission modes
Post from a hook or script$CLAUDE_CODE_MESSAGING_SOCKET + tokenNative Windows requires the auth line
Stop sending and listingDeny SendMessage and ListAgentsBoth take the bare tool name

Exercises

  1. Break the "later" assumption on purpose. Start two sessions. Send one message while both are alive. Then close the target, send again, and reopen a session with the same name. ✅ You pass when the message sent to the closed session never turns up in the reopened one, and your explanation names the socket rather than the clock.
  1. Set crossSessionInbound to refuse in one session and try to message it from the other. Then set hold, try again, and change the mode so the held message is released. ✅ You pass when you can describe all three outcomes — delivered, held, refused — and say which one takes your permission to become delivered.
  1. Predict before you test. Two sessions, one in bypassPermissions and one in the default mode, with no crossSessionInbound value set anywhere. Without running it, write down which direction ends up held. Then check. ✅ You pass when your prediction matches, or your note explains precisely which mode-class rule you had backwards.
  1. A teammate is not a peer session. Start an agent team (Phase 11, Lesson 73) and message a teammate. Then ask what the message can carry that a peer-session message can't. ✅ You pass when you can say which wire the teammate message uses and why structured protocol messages don't travel cross-session.

Key Terms

TermWhat people sayWhat it actually means
cross-session messaging"chat between agents"Text pushed into a running session's socket. Not mail, not stored
the inbox socket"the session's inbox"A per-session Unix socket or Windows named pipe. Gone when the session exits
SendMessage"the message tool"Claude's send call. Takes plain text, max ~1M characters same-machine
ListAgents"the address book"Discovery. Lists subagents, teammates, local sessions, and reachable remote ones
notify_when_idle"ping me when it's done"A one-shot notice when the watched session next goes idle or exits. 12-hour expiry
crossSessionInbound"a permission setting"accept / hold / refuse for arriving messages. The default comes from both sessions' permission classes
own-child message"a message from myself"A hook or Bash command posting into its own session. Needs process evidence, or the token
reply address"the return path"Attached to every message except a one-way cross-machine one

What this lesson skipped

Cross-machine and cloud delivery. Messages to your sessions on another machine, or in the cloud, travel through Anthropic servers over Remote Control and need a claude.ai sign-in. That is the provider-and-connectivity layer, and it is Phase 17's territory. Same-machine messaging, which is what this lesson teaches, never leaves your computer.

Channels. The other direction — an external event pushing into a session — is a different mechanism with its own contract. Lesson 83 shows where it sits next to hooks; the full contract is Phase 17.

Agent-team protocol messages. Teammates exchange structured messages that stay inside the team. Plain text is the cross-session wire.

Routines and scheduled tasks. Messaging is not scheduling, and Lesson 82 is the one that covers timers. The two combine, and it is worth knowing they are separate.

What a burst looks like when it fails. Rapid sends to one session get refused at the sender, and message loops are throttled at the receiver. If you build something chatty, read the limitations section of the page above before you design around it.

Check yourself

5 questions. Answer before you reveal — recognising the right option is not the same as being able to produce it.

  1. You renamed a session api-worker and sent it a finding. Ten minutes later you close that terminal. A second session now asks Claude to message api-worker. What actually happens?

    1. Nothing is delivered. The closed session has no socket, and no queue or retry keeps the message
    2. The message waits on disk and is delivered when you run claude --resume api-worker
    3. The message is routed through Anthropic servers and arrives when the session next starts
    4. The message is delivered to the second session instead, since the name no longer resolves
    Show answer

    A. Nothing is delivered. The closed session has no socket, and no queue or retry keeps the message

    The transport on this machine is a per-session socket bound by the running session. Close the session and there is no address, no store, and nothing to retry to. The tempting wrong answer is the --resume one, because resume genuinely does bring a session back — but it replays that session's own conversation, not other sessions' undelivered messages, and the docs describe no message store. Anthropic servers are only involved for sessions on another machine or in the cloud, which is the third option's error, and nothing ever silently redirects a message to a different session.

  2. Session B is mid-turn running a long npm test. Session A messages it. When does B's Claude read the message?

    1. Between B's tool calls, during the active turn — the running tool is never interrupted
    2. Immediately, by interrupting the running command so the message can be read
    3. Only after B's whole turn ends and the session goes fully idle
    4. Only when you approve it, because B is busy
    Show answer

    A. Between B's tool calls, during the active turn — the running tool is never interrupted

    Delivery happens between tool calls inside an active turn. The temptation is to pick the idle answer, because starting a new turn for an idle session is the other half of the documented behaviour — but that half only applies when B is already idle, not while it is mid-turn. Nothing interrupts a running tool, so the second option describes a preemption that does not exist. Approval is only involved for inbound controls such as a held message, which is about the message's origin and the session's mode, not about busyness.

  3. A message asks session B to run git push. B's settings already deny that command. What does B do?

    1. Treats it as plain text: commands in a message never execute, and B's own permission rules still decide anything the message asks for
    2. Runs it, because a message from another of your sessions carries your authority
    3. Refuses the whole message before Claude sees it, because it contains a command
    4. Executes the command but files the result as untrusted output
    Show answer

    A. Treats it as plain text: commands in a message never execute, and B's own permission rules still decide anything the message asks for

    Claude Code tells the receiving Claude the message came from another session, not from you: it cannot approve anything, cannot change configuration, and any /command-shaped text arrives as plain characters. The tempting wrong answer is the authority one, because the sender is a session you launched — but the trust boundary is per-session and the receiving side's own rules still apply, so the push would hit B's existing deny. Refusing the whole message is a real behaviour, but it belongs to the inbound controls based on the sender's permission class, not to the presence of a command in the text.

  4. You want to be told when a long-running local session finishes its current work, without checking on it. Which request can actually be honoured?

    1. From your main conversation, subscribe to an idle notice for a session on this machine
    2. From any subagent, subscribe to an idle notice for any session Claude can reach
    3. From your main conversation, subscribe to an idle notice for a cloud session on another machine
    4. From your main conversation, subscribe to a notice every time that session finishes any turn, for up to 30 days
    Show answer

    A. From your main conversation, subscribe to an idle notice for a session on this machine

    Only the Claude in your main conversation can subscribe, only to your sessions on this machine, and the notice fires once — when the watched session next goes idle or exits. The subagent version is tempting because Claude spends most of its life inside subagents, but the docs refuse the whole call for a teammate, subagent, or beyond-this-machine target, which also kills the cloud option. The last option gets both details backwards: it is one notice rather than every turn, and an unanswered subscription is dropped after 12 hours, not 30 days.

  5. A hook script running on native Windows wants to post a line into its own session's socket. What must it send before anything else?

    1. An auth line carrying the value of CLAUDE_CODE_MESSAGING_TOKEN, as the first line of the connection
    2. Nothing — the auth line is optional on every platform
    3. The socket path, so the session knows which inbox to use
    4. A {"type":"message"} line, after which the text follows
    Show answer

    A. An auth line carrying the value of CLAUDE_CODE_MESSAGING_TOKEN, as the first line of the connection

    On native Windows the auth line is required; a connection whose first line is not a valid auth line is closed and delivers nothing. The optional-on-every-platform answer is the trap, and it is half-true: on macOS and Linux, including WSL 2, the line is genuinely optional. The socket path is wrong because the caller chooses the socket to connect to — the session does not need to be told its own address. There is no documented {"type":"message"} handshake; the documented first-line types are auth and then message content.