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:

toDestinationDelivered
a name or agentId from ListAgentsanother agent of this conversation — one it launched, or a siblingqueued 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 machineits 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 fake User: 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:

EnvEffect
CLAUDIN_DISABLE_CROSS_SESSION=1no inbox, no peer-session rows in ListAgents, every send to a session refused with a reason
CLAUDIN_DISABLE_SEND_MESSAGE=1removes 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.