· 6 min read ·

Claude Code Channels: MCP Notifications as a Context Injection Mechanism

Source: hackernews

Claude Code v2.1.80 introduced channels as a research preview, and the framing in the announcement undersells what’s technically interesting about it. On the surface it looks like a webhook receiver. Under the hood it’s a specific reuse of MCP’s notification mechanism to inject structured context into Claude’s active session, and the design choices around that injection model have real consequences for how you build on top of it.

How the plumbing works

A channel is an MCP server, nothing more. Claude Code spawns it as a subprocess and speaks to it over stdio using standard JSON-RPC, the same transport mechanism every other MCP server uses. What distinguishes a channel from a regular tool server is a single capability declaration:

{
  "capabilities": {
    "experimental": {
      "claude/channel": {}
    }
  }
}

That flag tells Claude Code this server intends to push events, not just respond to tool calls. From that point on, the channel can fire notifications/claude/channel messages whenever something happens:

await mcp.notification({
  method: 'notifications/claude/channel',
  params: {
    content: 'build failed on main: https://ci.example.com/run/1234',
    meta: { severity: 'high', run_id: '1234' },
  },
})

Claude Code receives this and wraps it before inserting it into the context window:

<channel source="webhook" severity="high" run_id="1234">
build failed on main: https://ci.example.com/run/1234
</channel>

The meta fields from the notification params become XML attributes on the <channel> tag. The source attribute comes from how the channel registered itself. This is the entire delivery mechanism: structured XML appearing in Claude’s context, where Claude can read it and act.

The reason this works without a new protocol is that MCP’s notification primitive was always a server-to-client push; it just wasn’t being used for anything consequential in most implementations. Channels repurpose it as the event bus.

What the <channel> tag model actually means

Injecting content as XML tags into the context window is a deliberate design choice, not an implementation convenience. It means the event data is just text from Claude’s perspective, sitting in the same conversation space as everything else. Claude doesn’t need special handling to read a channel event; it sees the tag, reads the content, and can reason about it with the same capabilities it uses for anything else in context.

This is both the power and the risk of the model. The power: Claude can correlate a CI failure message with the code it’s already looking at, call tools to fetch logs, open files, and reason about what broke, all because the event landed in the same context where that work is happening. You’re not building a separate integration layer that parses events and dispatches commands; the event just arrives and Claude decides what to do.

The risk: anything that can get text into that XML tag can influence Claude’s behavior. The channels reference is direct about this. An ungated channel is a prompt injection vector. Anyone who can reach your channel endpoint can put arbitrary text in front of Claude.

The security model, and where most people will get it wrong

The official plugins (Telegram, Discord) handle this with a sender allowlist, and the critical detail is that you gate on sender ID, not room or channel ID. In group chats this distinction matters a lot. A room ID tells you where the message came from; a sender ID tells you who sent it. Those are not the same thing, and confusing them is how you end up letting any member of a shared Discord server inject into your Claude session.

The pairing flow enforces this correctly. You DM the bot, receive a pairing code, then approve it inside Claude Code:

/telegram:access pair <code>

After pairing, you lock it down:

/telegram:access policy allowlist

Once allowlisted, only messages from your sender ID go through. Messages from other users in the same group chat are dropped before they reach the channel notification mechanism. The room is irrelevant; only the verified sender matters.

For custom channels receiving webhooks from CI or monitoring systems, the equivalent is validating whatever identity token the sending system provides before calling mcp.notification. The channel is the gating layer. If your channel server forwards everything that hits the endpoint without verification, you’ve built an unauthenticated context injection API.

The session persistence problem

Channels only deliver events to an open session. There is no queuing, no replay, no daemon. If Claude Code isn’t running when your CI build fails, the notification goes nowhere.

For interactive use cases like the Telegram bridge, this is fine. You open a session, use it, close it. Events only matter while you’re present.

For always-on automation like CI failure investigation or monitoring alert response, you need to keep a session alive. The practical approach is running Claude Code in a background process, typically in a persistent terminal session under something like tmux or screen. The session stays open, events flow in, Claude works on them. It’s not a daemon in any formal sense; it’s just a long-lived interactive process you’ve chosen not to close.

This is a genuine constraint worth understanding before you design around it. You’re not deploying a service; you’re keeping a CLI session alive. The operational model is closer to leaving a terminal open than to running a background worker.

For webhook-driven use cases, there’s an additional networking consideration. Claude Code runs locally, and the channel MCP server listens on localhost. External systems like GitHub Actions or Datadog can’t POST to localhost directly. The standard pattern is a tunnel like ngrok that exposes a public URL and forwards to the channel server’s local port. The channel server is still yours to write; it just needs to accept the inbound HTTP and translate it to mcp.notification calls over the stdio connection to Claude Code.

Two-way channels and the reply mechanism

A channel that only receives is a one-way push. Two-way channels expose an MCP tool that Claude can call to send a reply back through the channel. For the Telegram plugin, this means Claude can respond to a message you sent from your phone while it’s working in the background. The tool call goes through the same MCP connection the channel notifications came in on, and the channel server handles delivery.

This is different from building a chat bot in the traditional sense. The reply mechanism is a side channel for communication, not the primary interface. Claude’s main work is still happening in the local session against local files and tools; the Telegram bridge is just how you check in on it or nudge it while you’re away from the machine.

What this is good for

The use cases that fit the architecture cleanly are the ones where you already have a running Claude Code session doing work, and you want to feed it information from outside without interrupting to paste things in. CI builds finishing, monitoring alerts firing, deploys completing. The event arrives, Claude sees it in context, and can act on it immediately using whatever tools and file access it already has open.

The cases that fit less well are the ones that need always-on reliability without babysitting a process, or multi-user workflows where more than one person needs to gate events. The research preview status is appropriate; this is the foundation of something more complete rather than a finished system.

The full reference documentation covers the channel server API in detail if you want to build a custom channel. The architecture is simpler than it looks from the outside: write an MCP server, declare the capability, call the notification method when events happen, and let Claude Code handle the rest.

Was this interesting?