Channels: notifications/claude/channel silently dropped in stream-json output mode (Claude Desktop)

Resolved 💬 3 comments Opened May 3, 2026 by Pol-Valentin Closed Jun 2, 2026

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

  1. 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.
  1. 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
\
\\

  1. While running, trigger an event from the channel server (e.g. by sending a message via whatever client the server listens to).
  1. Inspect the channel server's stderr or instrumentation: \mcp.notification\ is called with the expected payload.
  1. 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:

  1. Spawns claude.real with stream-json piped stdio
  2. Reads channel events from a side file written by a modified channel server (or another out-of-band mechanism)
  3. 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!

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗