Channels: notifications/claude/channel silently dropped in stream-json output mode (Claude Desktop)
Summary
In Claude Code 2.1.121, when running with \--input-format stream-json --output-format stream-json\ (the mode forced by Claude Desktop), \notifications/claude/channel\ notifications received from MCP servers declaring the \experimental.claude/channel\ capability are silently dropped: they are never emitted on stdout in any form, so the model never sees the corresponding \<channel ...>\ event.
The MCP server itself works correctly (the notification is sent over stdio without error). The issue is on the consumer side: \claude.real\'s stream-json output never carries channel events.
In interactive (TTY) mode with the same MCP server config and the same \--dangerously-load-development-channels server:foo\ flag, channels work as documented and the model receives \<channel source="foo" ...>\ tags in context.
Reproduction
- Configure a development channel server in \
~/.claude.json\per the Channels reference. Any MCP server declaring \capabilities.experimental['claude/channel']: {}\and emitting \mcp.notification({method: 'notifications/claude/channel', ...})\will do.
- Launch Claude Code in stream-json mode with the channel enabled:
\\\bash\
echo '{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"wait silently for 60s; if you receive a channel event, echo it verbatim\"}}' | \\
claude \\
--dangerously-load-development-channels server:my-channel \\
--print --input-format stream-json --output-format stream-json --verbose \\
--include-partial-messages \\
> /tmp/out.jsonl
\\
- While running, trigger an event from the channel server (e.g. by sending a message via whatever client the server listens to).
- Inspect the channel server's stderr or instrumentation: \
mcp.notification\is called with the expected payload.
- Inspect \
/tmp/out.jsonl\: no event with \type: \"channel\"\(or any equivalent) is present, and the comment text is nowhere in the output stream.
Types observed in stream-json output (none represent the channel event):
- \
system\(subtypes: \init\, \status\, \hook_started\, \hook_response\) - \
stream_event\ - \
message\, \message_start\, \message_stop\, \message_delta\ - \
content_block_start\, \content_block_delta\, \content_block_stop\ - \
assistant\, \user\, \thinking\, \signature_delta\ - \
result\, \rate_limit_event\
Expected behavior
When stream-json mode is active and a channel notification arrives, claude.real should either:
- Emit a stream-json event (e.g. \
{\"type\":\"channel\", \"source\":\"...\", \"meta\":{...}, \"content\":\"...\"}\) so consumers (Claude Desktop, custom IDE clients, etc.) can inject the \<channel>\tag into the model's context the same way the interactive terminal does, or - Inject the channel event into the model's input as if it were a normal user message containing the \
<channel ...>...</channel>\tag, surfacing as part of the existing \message\/ \content_block\events.
Observed impact
- Claude Desktop users cannot use any channel server (Markdown Reader, Claude Feedback browser extension, custom webhooks, fakechat, etc.). Tools exposed by the MCP server (e.g. \
reply\, \open_file\) continue to work because they go through the standard tool-call path; only the inbound notification path is broken. - Pro/Max users are affected even though channels are nominally available to them (no org policy in play, no \
channelsEnabled\block).
Configuration
- macOS 26
- Claude Code 2.1.121
- Authenticated via claude.ai (\
authMethod=claude.ai\, \subscriptionType=max\, no enterprise org) - Channel servers tested: custom local MCP servers (markdown-reader, claude-feedback browser ext) declaring \
experimental.claude/channel\and \tools\capabilities - All flags present in failing invocation (from Claude Desktop): \
--output-format stream-json --verbose --input-format stream-json --effort high --model claude-opus-4-7 --permission-prompt-tool stdio --allowedTools ... --setting-sources=user,project,local --permission-mode auto --allow-dangerously-skip-permissions --include-partial-messages --plugin-dir ... --replay-user-messages\
Workaround
For now, channel users on Claude Desktop need a wrapper proxy that:
- Spawns claude.real with stream-json piped stdio
- Reads channel events from a side file written by a modified channel server (or another out-of-band mechanism)
- Re-injects them as synthetic stream-json user messages containing the \
<channel ...>...</channel>\tag
This works end-to-end (model sees the event, calls the reply tool, response round-trips back to the channel client) but obviously shouldn't be necessary.
Bisection
Verified that the issue is strictly tied to stream-json mode, not to any individual flag besides \--input-format stream-json --output-format stream-json\:
- \
claude --dangerously-load-development-channels server:foo\(interactive): channels work ✅ - Same + \
--effort, --model, --permission-mode auto, --allow-dangerously-skip-permissions, --setting-sources, --permission-prompt-tool stdio, --allowedTools\(interactive): channels work ✅ - Add \
--print --input-format stream-json --output-format stream-json\: channels stop being emitted on stdout ❌
Happy to provide additional logs, repros, or instrumentation if useful. Thanks!
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗