Desktop app disables `SendMessage` via `--disallowedTools`, but `ListAgents` in the same session still lists peers and documents it as the address
Claude Desktop spawns every Claude Code session with --disallowedTools SendMessage, backed by a second permission-layer auto-deny. SendMessage is therefore unavailable in desktop sessions and in every subagent they spawn.
It worked on the desktop app through 2.1.258 (last successful subagent resume: 2026-09-02) and stopped at 2.1.260. There is no CHANGELOG entry, no deprecation notice, and no actionable error — ListAgents, the Agent tool description, and every subagent completion message still instruct the model to use the tool, so the model reaches for something that is not there and improvises around the gap.
This is not a version regression: running the desktop app's own bundled 2.1.260/claude.exe in CLI mode returns 44 tools including SendMessage, an identical set to 2.1.266. Only the launcher differs.
---
What broke: one flag, six mechanisms
SendMessage is not only "message another session". Six separately-documented mechanisms route through it and its notify_when_idle parameter. A single flag removed all six at once, and none of them failed loudly.
| Link in the chain | Documented mechanism | State now |
|---|---|---|
| Start a supervisor from the app | A supervisor session reports back on two channels by design: a file trail and a SendMessage notice to the dispatching session | Second channel gone. A supervisor started from the app cannot report at all; supervisors must move to claude --bg |
| Supervise running workers | notify_when_idle — one-shot subscription, no polling, "costs the other session nothing" | Gone. Supervision reverts to polling, which spends tokens on every check in a session that previously spent none |
| Converge a review over rounds | SendMessage({to: agentId}) to continue an existing agent | Gone on desktop. Each round is now a cold re-dispatch that rebuilds the reviewer's full context (~185K tokens in my case) |
| Escalate to the human | A scheduled task is the only clock that does not depend on a live session; it reports what it finds | Denied by the unattended_send_message branch. A watchdog that cannot report anything is not a watchdog |
| Recover after a usage-limit kill | Resume the terminated subagents instead of re-running them — files already written survive | Gone. Every usage-limit hit forces a full cold re-dispatch, so the retry costs more than the original run did |
| Correct a running background job | — | ListAgents lists bg rooms but no tool in the session can address them. The only way through is to shell out and spawn a separate CLI process purely to relay one message — verified working, but it is an undocumented workaround that burns an entire session per message. Otherwise a work order must be complete at dispatch and the only correction is claude stop |
The net effect is not one broken step. It is a supervision layer degrading from event-driven to polling-plus-manual, silently, in six places.
What it cost in practice
This was a working process, not a hypothetical. Since 2.1.260:
- every review round is a cold re-dispatch instead of a continuation
- one review round had to be re-scoped and re-run from scratch because the reviewer could not be continued
- each round's record now has to be authored to be self-contained, because no later round can ask the previous reviewer anything
- the supervisor role had to move out of the desktop app entirely, into
claude --bg— the only surface where the tool still exists
That last workaround is fragile by construction: it depends on an undocumented difference between two launchers of the same binary, which can close at any release with no notice — exactly the way this one did.
---
Why this reads as a bug, not a documented limitation
The shipped deny message says:
Usemcp__ccd_session_mgmt__send_messageto message another session from the desktop app; the CLI-nativeSendMessagetool is unavailable here (terminal sessions outside Claude Desktop are not reachable from this session).
Three things contradict the parenthetical:
- A desktop session did reach a
--bgroom, on 2.1.246 — see Evidence 4. - The transport still works today, in the other direction — see Evidence 5.
ListAgentsis still enabled in the same session and still lists those rooms, and its own description still namesSendMessageas the way to reach them — see Evidence 6.
And the substitute the message points at cannot address those rooms at all: mcp__ccd_session_mgmt__send_message only resolves sessions that appear in mcp__ccd_session_mgmt__list_sessions, which excludes --bg and terminal sessions. It returns session not found.
---
Environment
| | |
|---|---|
| Claude Desktop | 1.49585.0 (Windows 10 Pro 19045, MSIX) |
| Bundled Claude Code | 2.1.258, 2.1.260 (running 2.1.260) |
| Standalone CLI | 2.1.266 (~/.local/bin/claude) |
The desktop app bundles its own copy of Claude Code and updates separately from the standalone CLI.
---
Evidence
1. The flag comes from the desktop app, not from user config
Actual spawn command line (Get-CimInstance Win32_Process):
<AppData>\Roaming\Claude\claude-code\2.1.260\claude.exe
--output-format stream-json --verbose --input-format stream-json
--effort xhigh --model <model>
--permission-prompt-tool stdio
--allowedTools mcp__computer-use,mcp__ccd_session__spawn_task,…,
mcp__ccd_session_mgmt__send_message,…
--disallowedTools SendMessage
--setting-sources=user,project,local
--permission-mode auto --allow-dangerously-skip-permissions
The same command line disables the built-in tool and allow-lists the MCP substitute. No file under <AppData>\Roaming\Claude\ contains disallowedTools; the string lives in app.asar. --disallowedTools cannot be overridden from settings.json.
2. A second layer denies it independently of the flag
From app.asar:
if (e.tool_name === "SendMessage")
return t.ES("desktop_ccd_permission_auto_denied", {
session_id: r, tool_name: e.tool_name, reason: "cli_native_send_message"
}), { hookSpecificOutput: { hookEventName: "PreToolUse",
permissionDecision: "deny", permissionDecisionReason: `…` }};
Two sibling branches sit next to it, which is how the blast radius reaches beyond the desktop app:
reason: "unattended_send_message"
→ "This tool is unavailable in unattended sessions (scheduled-task runs and remote-dispatched trees)."
reason: "remote_dispatch_chain"
→ "This tool is unavailable in sessions dispatched by a remote orchestrator."
Runtime error observed on 2.1.260 (2026-09-06):
No such tool available: SendMessage.
SendMessage is disabled for this session, in subagents as well as here.
3. Same binary, different launcher — version is not the variable
Running the desktop app's own 2.1.260/claude.exe in CLI mode returns 44 tools including SendMessage and ListAgents — item-for-item identical to 2.1.266. The only difference between working and not working is which process starts it and with what flags.
4. Counter-evidence: a desktop session reached a --bg room
From a desktop-app session transcript (surface confirmed by its mcp__ccd_* tool calls), 2.1.246:
2026-08-28T13:14:26Z SendMessage to="<bg room display name>"
→ {"success":true,
"message":"\"…\" → <bg room display name> (another Claude session on this machine)"}
The target was a session started with claude --bg. The same session also resumed a subagent successfully at 13:45:30 ({"success":true,"message":"Resuming agent …"}).
An earlier attempt in that same session using the raw id failed (No agent named '<id>' is reachable) and succeeded once re-addressed by display name — an addressing-format issue, not a reachability one.
5. The transport is alive today, in both directions
Verified 2026-09-10:
- bg → desktop: a session started with
claude --bgsent a message to this desktop session over the daemon named pipe. Delivered, surfaced as a cross-session message. - desktop → bg: the recommended substitute returns
Message not delivered: session local_<uuid> not found.
Only the outbound half from the desktop is blocked.
6. The tool descriptions still advertise what is disabled
ListAgents output from this desktop session:
Peer sessions (3):
charter-f1 [9c1493] · interactive · started 1h ago
charter-68 [471f31] · interactive · started 7m ago
<bg probe> [293496] · bg · idle · started 7s ago
Its shipped description in that same session:
Lists agents you can SendMessage to — in-process subagents you spawned, … other local Claude sessions on this machine, … Names are the address: send with SendMessage({to: "<name>"}), copying the name exactly as a row prints it.
Every subagent completion message ends with:
agentId: a30e265c54747db17 (use SendMessage with to: 'a30e265c54747db17', … to continue this agent)
So the reachability information is present and the instructions are intact; only the send path is cut.
7. notify_when_idle is a parameter, not a separate tool
Full schema (dumped from a CLI session where the tool is available):
notify_when_idle — Ask a session ON THIS MACHINE to send you ONE notice when it next goes idle (finishes its turn with nothing queued) or exits — opt-in, one-shot, no polling. With a message: deliver it now AND subscribe. Without a message (omit it): a pure subscription that costs the other session nothing.
to— Recipient: a name fromListAgents…, a teammate name,"main", or a background agent's agentId
Two things follow. Disabling SendMessage also removes the only no-poll completion subscription, which has no substitute in the ccd MCP. And background agents are an explicitly documented recipient type, which is hard to square with "not reachable".
---
Reproduction
- Open a session in Claude Desktop.
ListAgents→ note abgorinteractivepeer is listed.- Ask the model to
SendMessageto that name → the tool is absent (ToolSearchreturns nothing) or denied. - In a terminal, run the desktop app's own bundled binary:
<AppData>\Roaming\Claude\claude-code\<ver>\claude.exe -p "list every tool you have"→SendMessageis present. claude --bg "use SendMessage to message <desktop session name>"→ delivered.
---
Asks
- Preferred — let desktop sessions address what
ListAgentsalready lists. The daemon roster resolves those rooms, the pipe transport demonstrably works in both directions, andSendMessage's own schema names background agents as a valid recipient. - Whatever happens to messaging, keep
notify_when_idlereachable. It is a distinct capability that happens to ride on the same tool, it has no substitute in the ccd MCP, and removing it converts zero-cost subscriptions into token-costing polling loops. The same applies to theunattended_send_messagebranch. - If the block is intended, make it legible. Update
ListAgents' description, theAgenttool description, and the subagent completion hint so none of them points at a tool disabled in that session; document the desktop-vs-CLI capability difference. - Either way, add a CHANGELOG line. There is currently none — which is why a working process broke silently rather than at a version I could pin.
---
Related
- #90481 — cross-session messaging disappearing after an update, labeled
bug/regression. Likely a different root cause: that report is VSCode/CLI-side with no--disallowedToolsin play. Filed separately to keep the two from being merged as duplicates. - #36196, #37051, #38183, #42737, #35240 — all report the same downstream symptom: the
Agentdescription points at aSendMessagethat is not available.