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:
- Startup blocks on it. The debug log shows no
Starting connection line for any MCP server while the dialog is up.
- 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.
- 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.
- 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.
- 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 |
Summary
The Claude driver materializes a channel-capable MCP server (
src/materialize.rs:393,expand_claudeinsrc/driver.rs) but there is no driver configuration in which a message ever reaches the model:dev-channels false, no channel flag is emitted, and Claude Code skips the channel withserver st2 not in --channels list for this session;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:
and the idle session woke on its own and ran a turn without any human input:
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-channelsshows this at startup:Two properties make this fatal for a supervised seat:
Starting connectionline for any MCP server while the dialog is up..claude.json— so it is re-asked on every launch.The upstream admission gate, and why
--channelsalone does not helpClaude Code 2.1.250's channel admission checks, in order: capability → protocol era → provider → feature availability → org policy →
--channelsmembership → allowlist. Measured outcomes for a plain stdio server:Channel notifications skipped: server ... not in --channels list for this session--channels server:XChannel notifications skipped: server ... is not on the approved channels allowlist (use --dangerously-load-development-channels for local dev)--dangerously-load-development-channels server:XChannel notifications registered+ deliverySo adding
--channels server:st2to the driver argv is not a fix: it moves the skip fromsessiontoallowlistand still delivers nothing. The final gate requires the channel entry to carrydev: true, anddev: trueis set in exactly one place — the merge of the entries parsed from--dangerously-load-development-channels.The allowlist mechanisms that avoid the dialog (
allowedChannelPluginsin managed settings, and the org/ledger fallback) are keyed on{plugin, marketplace}pairs. They have noserver:form, so a bare stdio MCP server has no sanctioned route.Two things that are not the problem, ruled out by measurement:
2025-11-25and is classifiedprotocolEra: "legacy", so the "modern protocol revision with no unsolicited notification path" skip does not apply. st2'sclaude_mcp.rsechoes the client's requestedprotocolVersionand ignores theserver/discoverprobe, which is what keeps it in the legacy era. No change needed there.capabilities.experimental["claude/channel"]as declared atsrc/claude_mcp.rs:59is accepted;claude/channel/permissionis 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.
st2 driver claude-mcpis 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 rejectdev-channels truefor seats that have no human at the terminal, instead of producing a seat that silently never receives mail or silently never boots.plugin:<name>@<marketplace>entry passed via--channels, with the pair listed inallowedChannelPluginsin 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_PATHdid not take effect in my harness). It is also a large change: a plugin, a marketplace, and an operator-owned managed setting.server:equivalent ofallowedChannelPlugins, 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
Must be run in a real terminal.
Versions
src/claude_mcp.rs,src/driver.rs(expand_claude),src/materialize.rs:393as ofmaintodayPosted on behalf of @schickling
agent_identitysessionagent_personaagent_supervisoragent_toolagent_tool_versionagent_runtimetooling_profile