Cross-machine `SendMessage` cannot be initiated: bridge session IDs are computed but never surfaced
Version: 2.1.224 (macOS, Claude Max, first-party API)
Summary
Cross-session messaging works well between sessions on the same machine. Across machines it cannot be initiated at all, because the only address that works is never surfaced in any tool output, CLI output, or UI — while the identifier that is surfaced is guaranteed to be rejected.
(The raw ID does get written to the debug log by the send path itself, but only after a successful send, so it cannot be used to obtain an address you do not already have.)
The deadlock
ListAgents lists Remote Control sessions from other machines. Sending to one by name is refused:
'<name>' matches a Remote Control session on this account, and those are
reply-only from here: messageable only in reply, after it messages you first.
The guidance is to wait for it to message you first. But the same refusal appears in the other direction — machine B sending to machine A by name gets the identical message about A. Neither side can be the one that "messages first", so the stated condition can never be satisfied by either party.
What actually works
A literal bridge address delivers immediately, both directions, no prompt:
{"to": "bridge:session_01XXXXXXXXXXXXXXXXXXXXXX", "message": "..."}
I confirmed this by reading bridgeSessionId out of ~/.claude/sessions/<pid>.json on one machine and pasting it into the other by hand. Once one message lands, the reply address arrives in the from attribute and the channel is two-way from then on.
So the transport works and the permission model works. The gap is entirely address discovery.
Where the ID goes
listAllPeers builds a usable address for every bridge row:
for (let d of u) c.push({ transport: "bridge", address: `bridge:${d.id}`, session: d });
It has exactly two consumers — the ListAgents tool and the /peers (/list-agents) slash command — and both discard address for bridge rows, rendering only title and status:
// formatForModel
n.map(({ session: i }) => GXo(BXo(i.title), [WLe, i.status]))
// formatForUser
case "bridge": return ` [${status}] · ${title} · ${WLe}` // WLe = "Remote Control"
For comparison, uds and subagent rows are looked up in a candidate index that can attach a [ref]. Bridge rows are never inserted into that index, so there is no code path that could attach an addressable handle to one — this isn't a flag that is switched off.
I checked for a way to turn it on: every tengu_* flag in the peer/bridge region, every CLAUDE_CODE_* environment variable, and the full settings schema. There is no setting that surfaces the ID. ListPeers is an alias on the same ListAgents tool, and the q / channel parameters are unwired (the implementation never reads its argument).
Two things that compound it
1. SendMessage's description denies the syntax exists. Its to documentation states:
the name IS the address; there is no separate address syntax
This is not accurate. validateInput has explicit branches for scheme === "bridge" and scheme === "uds", and both work when passed. So the one mechanism that solves the problem is documented as nonexistent.
**2. The tool that does document it is disabled.** SendFile's schema describes uds:<socket> / bridge:<session id> addressing, but SendFile.isEnabled returns false unconditionally.
Names genuinely cannot serve as cross-machine addresses — on this account ListAgents returns 168 Remote Control rows with duplicate titles (bug-hunter-comparison, mirror-remote-files-download each appear twice). Declining to resolve an ambiguous name is correct. The problem is that no unambiguous alternative is offered.
Reproduce
- Two machines on the same account, 2.1.224, Remote Control enabled on both.
- On A, run
ListAgents. Sessions from B appear as<name> · Remote Control · <status>— no ref, no ID. SendMessageto one of those names → reply-only refusal.- Do the same from B toward A → same refusal, mirrored.
- On A, read
bridgeSessionIdfrom~/.claude/sessions/<pid>.json. On B, send tobridge:<that id>→ delivered immediately.
Suggested fixes, smallest first
1. Include bridgeSessionId in claude agents --json. printAgentsJson already reads the local session record that contains this field, then copies out a whitelist that omits it. Adding it exposes only the local machine's own IDs — nothing about other machines, and no change to any permission boundary. That alone is enough for sessions to publish their own address to a shared location, which breaks the deadlock.
2. Correct the SendMessage to description so it documents uds: and bridge: addressing, which the validator already accepts.
3. Surface an addressable handle on Remote Control rows in ListAgents. If withholding it is deliberate — the refusal text mentions "device-gated" sessions, which suggests a considered boundary around one machine starting execution on another — then it would help to say so plainly, because the current wording instead points users toward an incoming message the other side is equally unable to send. Gating the handle behind isolatePeerMachines would surface it while keeping per-message approval mandatory.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗