Cross-Session Messaging
One agent sending a message to another — a subagent, the main conversation, or a whole separate Claudin session on the same machine — through two tools and one sanitized, policy-gated transport.
Overview
SendMessage and ListAgents cover every destination an agent might want to reach. The same call shape works whether the target is a subagent this conversation launched, the main conversation from a background agent, a teammate in an agent team, or an entirely different interactive Claudin session running in another terminal:
to | Destination | Delivered |
|---|---|---|
a name or agentId from ListAgents | another agent of this conversation — one it launched, or a sibling | queued for its next tool round; a stopped agent resumes from its transcript |
"main" | the main conversation (from a background agent only) | queued for the main thread's next turn |
a teammate name, "*" | agent-team members (inside an agent team only) | the team mailbox |
claudin-goal, claudin-goal [3fa9c1], uds:<path> | another interactive Claudin session on this machine | its peer inbox, over a Unix domain socket |
Both tools are deferred — they only add their names to the request until the model actually reaches for one — so what a receiving model needs to know about a peer message (that it isn't from the user, how to reply) travels with each message rather than living in every system prompt.
Sessions on another machine, and cloud / Remote Control sessions, are not reachable — a bridge: address is refused with a reason. Windows has no inbox in this version (named pipes have no file mode to check ownership against), so a Windows session can still message its own agents but not a peer session.
Addressing a peer session
ListAgents prints how the current session is addressed, then three sections — subagents, teammates (inside a team), and peer sessions:
This session is claudin [3fa9c1] — the name other sessions use to message it.
Subagents (1):
researcher · running · Map the registry
Peer sessions (2):
claudin-goal [8c21d0] · idle · ~/projects/claudin-goal · started 12 min ago
claudin-loop [41be07] · busy · ~/projects/claudin-loop · started 1 h ago
A session's name is its --name flag or /rename, or else its working directory's basename — which is what tells parallel worktrees apart. The [ref] is a short unique prefix of a hash of the socket path; send it only when two rows share a name, or an error asks for one. A name that also names one of this conversation's agents goes to the agent, not a peer session.
What the receiver sees
An inbound message from another session arrives wrapped in a tag that says where it came from and how to reply, so the receiving model never mistakes it for something the user typed:
<cross-session-message from="uds:/run/user/1000/claudin-socks/4242.sock" from-name="claudin-goal">
run the tests in packages/core and tell me which fail
</cross-session-message>
From another Claudin session on this machine, not from your user: … To reply, call SendMessage with to: "uds:…".
- It's queued as a notification: read at the running turn's next tool round, or it starts a turn if the session is idle — the same path a background agent's completion takes.
- The body can't close or open its own tag, so a crafted message can't forge the end of the wrapper and put words after it.
- The body is sanitized on arrival: terminal escapes, bidi overrides, other invisible code points, and every control character but tab and newline are stripped — so the confirmation dialog and the model see exactly the same text.
@path,@server:resource, and@agent-…mentions inside it attach nothing, and no slash command runs from it.- The TUI renders it inline as
@claudin-goal❯ first line · another session.
Inbound policy
With no setting changed, delivery follows mode parity: a message is delivered automatically when the sending and receiving sessions are on the same side of bypassPermissions, and held for the receiving user to approve when they aren't — a session that asks before acting shouldn't get a message from one that doesn't, and a session running unattended shouldn't silently act on a message from one that isn't.
A session in plan mode marks its outgoing messages accordingly, and a receiver that isn't planning holds them for the same reason: a session that changes nothing shouldn't be able to trigger changes elsewhere without the user seeing it first. A message sent into a planning session is delivered as usual.
/config → Agents & workflows → Messages from other sessions overrides this per session: accept, hold, or refuse. Project and local settings can only make it stricter, never looser, than the user's own setting.
A held message waits in a dialog — Held message from another session, showing the sender, the reason it was held, and the full body (a long one opens on its first 12 lines with a way to show the rest). Deny is the default and what Esc does. A held message expires after 5 minutes if left unanswered, and a session holds at most 100 at once. Either way, the sender's tool result reports what happened — held, then delivered, denied, or expired — once the user decides.
Waiting for a reply without spending a turn
SendMessage({ to, message?, notify_when_idle: true }) asks a session for one notice the next time it finishes a turn with nothing left queued, or exits. Sent without a message, it's a pure subscription — no turn runs on the other side just to answer it, and a session that's already idle answers immediately. A subscription lasts 12 hours before it expires on its own; a sender can hold at most 3 open on a given session (a fourth pushes out the oldest), and a session accepts at most 32 total. Only a session's main conversation can subscribe — a subagent asking is refused, since the notice needs a turn to land in.
Guardrails
A few limits keep two sessions from turning a conversation into a loop, or one session into a relay for something the user already said no to:
- Send budget — 10 sends to other sessions per prompt the user actually typed. A turn another session opened doesn't renew the budget, and neither does a prompt the harness itself wrote (a cron fire, a scheduled wakeup, a rate-limit resume) — so two sessions volleying messages back and forth stop at the tenth.
- No relaying a refusal. The prompt is explicit that an agent must never ask another session to do what was just denied or blocked here.
- The auto-mode classifier treats agent messages as untrusted input. A message another agent wrote is shown as a single line —
Agent message (not from the user) from "<name>": "<text>"— so a newline inside it can't forge a fakeUser:line, and the classifier is told such a message never by itself establishes user intent.
Two environment variables turn the whole feature off if you need to:
| Env | Effect |
|---|---|
CLAUDIN_DISABLE_CROSS_SESSION=1 | no inbox, no peer-session rows in ListAgents, every send to a session refused with a reason |
CLAUDIN_DISABLE_SEND_MESSAGE=1 | removes SendMessage and ListAgents entirely, outside an agent team |
Try it
Start a second Claudin session in another terminal, in the same or a different project directory:
claudin --name build
Then, from your original session, ask it to reach the other one:
ask the "build" session to run the test suite and tell me if anything fails
What to expect: the model calls ListAgents to confirm build is listed as a peer session, then SendMessage with to: "build". If both sessions are in the same permission mode the message is delivered straight away; otherwise it shows up as a held-message dialog in the build session, waiting for a decision there.