Skip to content

Claude driver: inbox messages can never be delivered — the channel is either unregistered or gated on a per-launch consent dialog an unattended seat cannot answer #373

Description

@schickling-assistant

Summary

The Claude driver materializes a channel-capable MCP server (src/materialize.rs:393, expand_claude in src/driver.rs) but there is no driver configuration in which a message ever reaches the model:

  • with the default dev-channels false, no channel flag is emitted, and Claude Code skips the channel with server st2 not in --channels list for this session;
  • with dev-channels true, the only flag emitted is --dangerously-load-development-channels=server:st2, and Claude Code puts a full-screen consent dialog in front of session startup that an unattended seat cannot answer — the seat never boots, and MCP servers do not even begin connecting until it is answered.

Measured on claude-code 2.1.250. Minimal reproduction (independent of st2, ~30 line stdio MCP server): https://github.com/schickling-repros/2026-08-claude-code-channel-server-consent

The mechanism works — this is worth stating first

Once the consent dialog is accepted, the whole path works end to end, including the part that was previously assumed dead:

MCP server "chanrepro": Channel notifications registered
MCP server "chanrepro": notifications/claude/channel: CHANNEL-PROBE-DELIVERED

and the idle session woke on its own and ran a turn without any human input:

⇥ chanrepro: CHANNEL-PROBE-DELIVERED
  I see a channel probe message rather than a task. No action needed on my end — …
  Cogitated for 2s

The MCP child process stayed alive for the whole idle session (45s, no turn running) and the notification was delivered while the session sat at an empty prompt. So the objection that the MCP child is absent/torn down while a session is idle does not hold on 2.1.250 — channel delivery genuinely does wake an idle agent.

Why it cannot be used unattended

--dangerously-load-development-channels shows this at startup:

WARNING: Loading development channels
--dangerously-load-development-channels is for local channel development only. Do not use
this option to run channels you have downloaded off the internet.
Please use --channels to run a list of approved channels.
Channels: server:st2
❯ 1. I am using this for local development
  2. Exit
Enter to confirm · Esc to cancel

Two properties make this fatal for a supervised seat:

  1. Startup blocks on it. The debug log shows no Starting connection line for any MCP server while the dialog is up.
  2. The consent is not persisted — nothing is written to .claude.json — so it is re-asked on every launch.

The upstream admission gate, and why --channels alone does not help

Claude Code 2.1.250's channel admission checks, in order: capability → protocol era → provider → feature availability → org policy → --channels membership → allowlist. Measured outcomes for a plain stdio server:

driver argv debug log
(none — st2's default) Channel notifications skipped: server ... not in --channels list for this session
--channels server:X Channel notifications skipped: server ... is not on the approved channels allowlist (use --dangerously-load-development-channels for local dev)
--dangerously-load-development-channels server:X blocking dialog; after accept → Channel notifications registered + delivery

So adding --channels server:st2 to the driver argv is not a fix: it moves the skip from session to allowlist and still delivers nothing. The final gate requires the channel entry to carry dev: true, and dev: true is set in exactly one place — the merge of the entries parsed from --dangerously-load-development-channels.

The allowlist mechanisms that avoid the dialog (allowedChannelPlugins in managed settings, and the org/ledger fallback) are keyed on {plugin, marketplace} pairs. They have no server: form, so a bare stdio MCP server has no sanctioned route.

Two things that are not the problem, ruled out by measurement:

  • Protocol era. The connection negotiates 2025-11-25 and is classified protocolEra: "legacy", so the "modern protocol revision with no unsolicited notification path" skip does not apply. st2's claude_mcp.rs echoes the client's requested protocolVersion and ignores the server/discover probe, which is what keeps it in the legacy era. No change needed there.
  • The channel capability itself. capabilities.experimental["claude/channel"] as declared at src/claude_mcp.rs:59 is accepted; claude/channel/permission is optional and its absence does not block delivery.

Also worth recording: the channel gate is not evaluated at all in -p (print) mode. Any future test of this must drive a real TTY.

Options

I did not pick one — reshaping or removing the claude-mcp channel path interacts with the accepted decision that made agent liveness a supervised sidecar, so it should be a deliberate call.

  1. Make the dead capability legible. Today st2 driver claude-mcp is materialized and connected on every Claude seat, polls the inbox every 250ms, and can never deliver. The driver could refuse (or warn loudly) when it materializes the channel server without a route, and reject dev-channels true for seats that have no human at the terminal, instead of producing a seat that silently never receives mail or silently never boots.
  2. Ship the channel server as a marketplace plugin. This is the only route the upstream code offers that avoids the dialog: a plugin:<name>@<marketplace> entry passed via --channels, with the pair listed in allowedChannelPlugins in managed settings. Note this is a code reading, not validated end to end — I could not construct a managed-settings tier in an isolated config to test it (CLAUDE_CODE_MANAGED_SETTINGS_PATH did not take effect in my harness). It is also a large change: a plugin, a marketplace, and an operator-owned managed setting.
  3. Ask upstream for a server: equivalent of allowedChannelPlugins, or for the development-channel consent to be persisted. Filed as the repro above; happy to take it to the Claude Code tracker.

Option 1 is the only one that is squarely st2's to make and is a strict improvement regardless of which way 2/3 go. Say the word and I'll open it.

Reproduction

git clone https://github.com/schickling-repros/2026-08-claude-code-channel-server-consent
cd 2026-08-claude-code-channel-server-consent
./run.sh none       # session skip
./run.sh channels   # allowlist skip
./run.sh dev        # blocking consent dialog; accept it and delivery works

Must be run in a real terminal.

Versions

  • claude-code 2.1.250
  • st2 src/claude_mcp.rs, src/driver.rs (expand_claude), src/materialize.rs:393 as of main today
  • Linux
Posted on behalf of @schickling
field value
agent_identity dev3.direct.claude.paqjmjfq
session dev3.paqjmjfq
agent_persona generalist
agent_supervisor unavailable
agent_tool Claude Code
agent_tool_version 2.1.250
agent_runtime Claude Code 2.1.250
tooling_profile dotfiles@a1a5f89

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions