SendMessage returns success:true before the inbound decision; held/expired notice only arrives ~5min later (dialogExpiry)
Summary
SendMessage returns {"success":true} as soon as the message is written to the peer's
inbox socket, before the receiving session's inbound controls have decided whether to
deliver, hold or refuse it. When the message is subsequently held and expires, the sender is
told — but only on the dialogExpiry deadline (default 5 minutes).
For short-lived claude -p workers this means the failure is always reported minutes after
the worker it concerns has exited, so the sender learns about it far too late to retry or
route around it.
Environment
- Claude Code 2.1.226
- macOS (Darwin 25.6.0)
- Both sessions on the same machine, same OS user
Reproduction
- From an interactive session running
--dangerously-skip-permissions(i.e. the
bypass-permissions class), start a short-lived worker in the prompting class:
``bashsleep 12
claude -p 'Run the bash command three times in sequence, one tool call each.``
Then output ONE line: RESULT=<yes|no>|<text of any cross-session message you received, or none>.' \
--name eval-plain --allowedTools "Bash(sleep:*)" < /dev/null
- While it is running, have the interactive session
SendMessagetoeval-plainwith a
distinctive probe token.
- Observe the worker's output, then wait five minutes.
Actual behaviour
SendMessagereturns immediately:
{"success":true,"message":"“…” → eval-plain (another Claude session on this machine)","msg_id":"…"}
- The worker never receives the message and prints
RESULT=no|none. This is correct per the
documented default (bypass sender → prompting receiver = held), and a -p session cannot
render the approval dialog, so it can never be approved.
- The worker exits (measured at 16s, and 56s on a re-run).
- ~5 minutes after the send, the sender receives:
````
Your message to another session was held for the recipient user's approval
(recipient: uds:/tmp/cc-socks/12823.sock). Not delivered to that session's Claude yet.
Your message to another session was not approved before expiry
(recipient: uds:/tmp/cc-socks/12823.sock). Not delivered to that session's Claude.
Control: the identical worker started with --settings '{"crossSessionInbound":"accept"}'
receives the probe immediately, confirming the hold — not a timing artefact — is the cause.
A re-run of the default arm living 56s (longer than the 46s successful control) still
received nothing.
Expected behaviour
The sender should be able to distinguish "queued for delivery" from "held pending approval"
at call time. Concretely, either:
SendMessagereturns a status reflecting the inbound decision (e.g.
{"success":true,"state":"held"} vs "delivered"), since that decision is made by the
receiver on arrival and does not require waiting for the dialog outcome; or
- the held notice is emitted to the sender promptly on arrival, rather than being batched
with the expiry notice at the dialogExpiry deadline.
Today both notices appear together, ~5 minutes later, so success:true is the only signal
available within the lifetime of a short-lived peer.
Why it matters
The documented use case — "get status from long-running work", a -p worker reporting into
a watched session — is exactly where this bites. A caller cannot tell a delivered message
from a silently held one until long after the peer is gone, so any wrapper or workflow built
on SendMessage can stall with both ends believing the exchange succeeded.
Minor, possibly related
The delivery notices identify the peer by socket path (uds:/tmp/cc-socks/<pid>.sock) rather
than by the session name the message was addressed to. Once the process has exited, that path
is the only handle the sender has, so attributing a failure back to a named worker requires
having recorded the PID→name mapping yourself at spawn time. Including the target name in the
notice would make these self-describing.