--dangerously-load-development-channels does not register bare server: channels (inbound notifications silently dropped)
Summary
A .mcp.json MCP server that declares capabilities.experimental["claude/channel"] connects fine and its tools work (outbound reply succeeds), but inbound channel notifications are silently dropped when the session is launched with --dangerously-load-development-channels server:<name>.
Every session, --debug logs:
MCP server "myserver": Channel notifications skipped: server myserver not in --channels list for this session
This is the kind:"session" gate — the server has already passed the capability check, the tengu_harbor feature-flag gate, auth (claude.ai OAuth, confirmed logged in), and the org-policy gate. It fails purely because the dev-flag server was never registered into the session channel list.
Decompiling the binary (v2.1.195) shows the cause: the startup flag-assembly code parses the --dangerously-load-development-channels list but never registers it — it only registers the --channels list and then sends the dev-flag list to telemetry. The result is that --dangerously-load-development-channels is effectively a no-op for a bare server:<name> channel: outbound works, inbound is never delivered, and there is no working configuration on this version.
Environment
- Claude Code v2.1.195 (current latest as of this report)
- macOS, arm64
- Authenticated via claude.ai OAuth (not an API key), logged in and confirmed
- A third-party MCP server (declared in
<project-root>/.mcp.json) that advertises theclaude/channelcapability. This class of server exists in the wild as community channel bridges (Slack/Telegram/Discord, etc.); the bug reproduces with any bareserver:channel.
Repro steps
- Add a minimal stdio MCP server to
<project-root>/.mcp.jsonthat advertises the channel capability:
``json``
{
"mcpServers": {
"myserver": {
"command": "node",
"args": ["./my-channel-server.js"]
}
}
}
The server, in its initialize response, declares:
``json``
{
"capabilities": {
"experimental": { "claude/channel": {} }
}
}
and exposes an outbound reply tool.
- Launch Claude Code with the development-channels flag:
````
claude --dangerously-load-development-channels server:myserver --debug
- Observe:
- The server connects, and its tools (including outbound
reply) work normally. - No inbound channel notification (
notifications/claude/channel) is ever delivered to the conversation. - The debug log shows the skip line every session (see Debug evidence).
Expected vs Actual
Expected: Passing --dangerously-load-development-channels server:myserver registers myserver as a development channel for the session, so inbound notifications/claude/channel messages are delivered to the conversation (this is the documented local-dev path for channels).
Actual: Inbound notifications are silently skipped every session. The server is never added to the session channel list, so the session gate never matches it. Outbound tools still work, which makes the failure look like a connectivity issue when it is actually a registration gap.
Debug evidence
With --dangerously-load-development-channels server:myserver:
MCP server "myserver": Channel notifications skipped: server myserver not in --channels list for this session
If you instead ALSO pass --channels server:myserver (so it does get registered), it clears the session gate but then fails the next gate, because the entry is registered as non-dev and a bare server: channel has no settings-based allowlist:
MCP server "myserver": Channel notifications skipped: server myserver is not on the approved channels allowlist (use --dangerously-load-development-channels for local dev)
...even though --dangerously-load-development-channels server:myserver is also present on the same command line — because that flag's bypass never takes effect (its entries were never registered as dev entries).
Root cause
From the de-minified v2.1.195 startup flag-assembly code. Both flags are parsed with the same tokenizer, but only --channels is registered into the session channel list; the dev-flag list is parsed and then used only for telemetry:
const channels = opts.channels; // --channels
const devChannels = opts.dangerouslyLoadDevelopmentChannels; // --dangerously-load-development-channels
let parsedChannels = [];
if (channels?.length) {
parsedChannels = parseChannels(channels, "--channels");
registerChannels(parsedChannels); // ← registered
}
let parsedDev;
if (!isNonInteractive && devChannels?.length) {
parsedDev = parseChannels(devChannels, "--dangerously-load-development-channels"); // ← parsed...
}
if (parsedChannels.length || parsedDev?.length) {
telemetry("tengu_mcp_channel_flags", { /* counts + plugin names only */ }); // ← ...only used here
}
// NOTE: there is no registerChannels(parsedDev) call — the dev-flag entries never enter the session channel list.
The session-gate matcher then looks the server up in the registered list and finds nothing:
function findEntry(name, list) {
const parts = name.split(":");
return list.find(e =>
e.kind === "server"
? name === e.name
: parts[0] === "plugin" && parts[1] === e.name);
}
// list never contains the dev-flag server → returns undefined → skip kind:"session"
The downstream allowlist gate confirms the consequence: when the entry is registered (via --channels) it is marked non-dev, so it fails the allowlist gate, and for a bare server: channel there is no settings-based allowlist (only allowedChannelPlugins exists, which is plugin-only). So neither path works.
Impact / why there is no workaround on this version
--dangerously-load-development-channels server:<name>is a no-op for inbound on bareserver:channels — its parsed entries are never registered, so the session gate always skips.- Adding
--channels server:<name>registers the entry but as non-dev, so it then fails the approved-allowlist gate. - There is no
allowedChannelPluginsequivalent for bareserver:channels (that setting is plugin-only), so the allowlist gate cannot be satisfied either.
Net: on v2.1.195 there is no working configuration to receive inbound channel notifications for a bare server: channel. The documented local-dev path is broken.
Suggested fix
Register the parsed dev-flag entries into the session channel list, marked as development entries so they bypass the approved-allowlist gate — i.e. add the missing call:
if (!isNonInteractive && devChannels?.length) {
parsedDev = parseChannels(devChannels, "--dangerously-load-development-channels");
registerChannels(parsedDev, { dev: true }); // ← currently missing
}
With the entries registered as dev, they clear the kind:"session" gate (now present in the list) and bypass the allowlist gate (marked dev), restoring the intended local-dev behavior.
Possibly related
- #36503 —
--channelsplugin shows "Channels are not currently available" but inbound notifications are ignored (open; plugin-path variant of an inbound-drop). - #51845 —
--channelsflag never matches plugin server identifier, channel notifications always skipped (closed; same family of session-gate/identifier-matching failures, plugin-scoped).
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Independent confirmation on Linux (not macOS-only) — still present in v2.1.197
Confirming this exact bug from a second, unrelated MCP server. The root-cause decomposition above matches what we see; adding two data points that may help triage:
1. It's not macOS-specific. This issue is labeled
platform:macos, but we reproduce the identical failure on Linux x86_64 (Arch, kernel 7.0.9), Claude Code v2.1.197 (two patch releases after the reported v2.1.195), same claude.ai OAuth auth mode. The--dangerously-load-development-channels server:<name>registration gap is platform-independent — suggest widening the label.2. Different consumer class, same failure. Our server (
mcp-agent-relay) isn't a chat bridge — it's a job relay: one Claude session dispatches async work to another agent and expects a push (notifications/claude/channel) when the job reaches a terminal state. Outbound tools (dispatch/poll) work perfectly; the inbound channel push is silently dropped with the same debug line:Launched via
claude --dangerously-load-development-channels server:agentrelay. Server declarescapabilities.experimental["claude/channel"]and emitsnotifications/claude/channelon terminal transitions — never delivered.3. Workaround for anyone blocked today. Since the push path is dead, we moved wake-up to a pull-side
Stophook:SessionStartseeds a baseline of already-seen jobs, andStop/SubagentStopsurface any new terminal transitions (with an optional bounded long-poll so the session waits for an in-flight job instead of settling early). No dev flag, no per-session dialog, works with bothclaude mcp addand plugin installs. Not as clean as a real push, but it's a functional substitute until the registration gap is fixed.Happy to share a minimal relay repro if useful.
Confirming for 2.1.205
Channel notifications never registered for
server:entries, even with the documented flag combinationSummary
A channel MCP server (declared via
.mcp.json, connected via--dangerously-load-development-channels server:<name>and--channels server:<name>) connects successfully, butnotifications/claude/channelsent by that server are silently dropped.--debug mcpshows:— even though
--dangerously-load-development-channelswas passed with that exact server name. This makes the channels feature (research preview) completely non-functional for any locally-developedserver:-kind channel, as documented in https://code.claude.com/docs/en/channels-reference.Environment
2.1.205(also reproduced on2.1.204before an auto-update)v20.20.26.17.0-35-generic(Ubuntu 24.04)@modelcontextprotocol/sdk:1.29.0Minimal repro
This is Anthropic's own "webhook receiver" example from the channels reference docs, trimmed to the minimum needed:
Run:
Accept the "New MCP server found" prompt if shown. Wait a few seconds, then check the debug log:
Actual result
The
<channel source="repro" ...>tag never appears in the session — Claude never sees the pushed notification.Expected result
Per the channels reference:
should be sufficient to load a local development channel and have its notifications delivered. Instead, the server-kind entry is treated as not dev-approved even when explicitly passed to
--dangerously-load-development-channels.Notes from investigating further
--dangerously-load-development-channels server:reproalone (no--channels) produces a different skip reason:server repro not in --channels list for this session— meaning the dev flag alone never adds the entry to the session's effective allowed-channels list at all.server:reprovalue (the combination the docs suggest) produces the "not on the approved channels allowlist" skip shown above — i.e. an entry is found, but it's not marked as dev-approved.enabledMcpjsonServersin~/.claude.json(no prompts pending at all). Same skip message in both.node_modules-style build), the relevant lookup is roughlylist.find(entry => matches(name, entry)), and entries appear to be appended to the effective list separately for--channelsand--dangerously-load-development-channels. If both flags produce a separate entry for the same server name, and the non-dev entry happens to be checked/found first, the dev flag's entry is effectively never consulted. This is a guess based on reading obfuscated output, not confirmed against source — but it would fully explain the observed behavior.Impact
The channels research-preview feature is unusable for any
server:-kind (i.e. non-plugin, locally developed) channel on this build, despite following the documented setup exactly. This blocks building and testing custom channel integrations entirely, since even the canonical docs example doesn't work as written.Same behavior with the plugin: form on 2.1.220 (Windows 11). A marketplace-installed channel plugin launched with
--dangerously-load-development-channels plugin:<name>@<marketplace>connects, its tools work, outbound reply works, but inbound notifications are dropped with "Channel notifications skipped: server plugin:<name>:<server> not in --channels list for this session". The plain--channelsflag warns the plugin is not on the approved allowlist and points at this dev flag, andallowedChannelPluginsin managed settings changes nothing. So the flag currently isn't an escape hatch for either the server: or the plugin: form. Filed #82571 for the broader allowlist question.