Skip to content

Spawned Claude workers have an unobserved inbound path via Claude Code cross-session messaging #1463

Description

@khaliqgant

Summary

Claude Code v2.1.224+ ships cross-session messaging: every session binds a per-session Unix inbox socket and can receive plain-text messages from any other Claude Code session owned by the same OS user on the same machine. It is on by default with nothing to enable.

This means every Claude worker relay spawns now has a second inbound message path that relay does not control, does not log, and does not observe. relay sets no crossSessionInbound value, so the behavior falls back to Claude Code's per-message default, which is derived from the two sessions' permission modes — and relay puts all its Claude workers in the worst bucket for that default.

Why relay is affected specifically

relay auto-injects --dangerously-skip-permissions into every spawned Claude agent:

  • crates/broker/src/worker.rs:540 (spawn path)
  • crates/broker/src/worker.rs:771 (respawn path)
  • crates/broker/src/runtime/headless.rs:19 (claude -p --dangerously-skip-permissions)

That places every relay Claude worker in Claude Code's bypassPermissions class. Per the docs' default rules:

The receiving session bypasses permission prompts: Claude Code holds each message for your approval. It delivers one only when the sending session identifies itself as also bypassing.

So an inbound message from an ordinary human session is held, and an approval dialog opens inside a PTY that relay owns and no human is watching. It is dropped after dialogExpiry (5 min default). For the headless path it is worse — the docs state:

A -p session can't show the approval dialog. A held message stays held there.

Held indefinitely, against a 100-message hold cap.

Concrete problems

  1. Unobserved ingress. Anything that is delivered over the inbox socket reaches the agent without passing through relay's conversation_log, replay buffer, dedup, priority queue, or telemetry. Message provenance in a relay transcript becomes incomplete — a worker can act on an instruction that has no relay-side record.
  2. Silent, mode-dependent behavior. Whether a message is delivered, held, or dropped depends on the sender's permission class, not on any relay policy. Nothing surfaces this in relay's UI or events.
  3. Bypass-to-bypass is auto-delivered. Two relay workers, or any other bypass-mode session on the machine, can inject into each other with no approval step and no relay involvement.
  4. Escape hatch around relay policy. Whatever gating relay applies to message_dm_send does not apply here.

Proposal

Set crossSessionInbound explicitly at the three spawn sites, via the spawned session's --settings value, rather than inheriting the mode-derived default.

Default to refuse: relay should be the sole ingress for relay-managed workers, so every message a worker acts on stays accounted for in relay's log and telemetry. refuse from the --settings flag drops inbound peer messages without delivering them; the session still binds its socket and still appears in /list-agents, so there is no visible breakage, only a closed side channel.

Make it overridable per agent (spawn option / teams.json) for anyone who deliberately wants a relay worker reachable from their own Claude session, in which case accept is the correct value — the mode-derived default is never the right answer for an unattended worker, since it resolves to "hold for a human who isn't there."

Follow the existing precedent in worker.rs: the bypass flag is only injected when the caller has not already supplied one. Do the same here — do not clobber a crossSessionInbound an operator passed explicitly.

Notes / non-goals

  • This is about relay's inbound posture only. Whether relay should adopt SendMessage/CLAUDE_CODE_MESSAGING_SOCKET as a delivery transport for Claude→Claude is a separate question, and the answer is no: the socket wire format is undocumented and unversioned, the feature is absent on native Windows and on Bedrock/Foundry, it is silently disabled when DISABLE_TELEMETRY / DO_NOT_TRACK / CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC / DISABLE_GROWTHBOOK suppress feature-flag evaluation, and it rate-limits, de-duplicates, and caps queued messages at 50 per session — all fine for human-authored notes, all wrong for sustained orchestration. relay's PTY injector already provides readiness detection, retry, a bounded priority queue, and delivered/failed reporting that this path does not.
  • Affects macOS and Linux only; the feature does not exist on native Windows.

Verification

Per the repo standing order, the fix needs a test that bites: assert the spawn argv for a Claude worker carries the crossSessionInbound setting, then mutate the spawn builder to drop it and paste the failing transcript in the PR. A live check is also cheap — start a relay Claude worker, SendMessage to it from an ordinary interactive session, and confirm the message is refused rather than silently held.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions