# Claude Code channels: push events into your session

> Channels are MCP servers that push Telegram, Discord, iMessage, or webhook events into your open Claude Code session. Needs v2.1.80+ and an allowlist.

URL: https://bartlomiejkrupa.dev/articles/claude-code-channels/
Published: 2026-07-08
Updated: 2026-07-08
Tags: claude-code, channels, mcp, webhooks, telegram
Summary: Channels are MCP servers that push Telegram, Discord, iMessage, or webhook events into your open Claude Code session. Needs v2.1.80+ and an allowlist.

A CI job fails while you're at lunch. Your Telegram bot gets a DM. A deploy webhook fires. Channels are how those events land in the Claude Code session already running on your machine - with your repo open and your context intact - instead of spinning up a cloud sandbox or hoping Claude polls an MCP server on its own.

Two gates matter before anything injects: the server has to be in `.mcp.json` and named on `--channels`, and only allowlisted senders get through.

## Definition

**Channel** - an MCP server that declares `claude/channel`, runs as a Claude Code subprocess over stdio, and fires `notifications/claude/channel` when something happens outside the terminal. One-way channels forward alerts. Two-way ones (Telegram, Discord, iMessage) also register a `reply` tool so Claude answers on the same platform.

Nothing arrives after you quit the session - channels only fire into a live process. Keep one running with a persistent terminal or a background `claude` process.

## Where channels sit

Claude Code has several ways to talk to the outside world. Channels own one niche: push into an open local session.

| Feature | What it does | Good for |
| --- | --- | --- |
| **Channels** | Push events into your running local session | Chat from your phone, CI/webhook into work already in progress |
| Claude Code on the web | Fresh cloud sandbox from GitHub | Async tasks you check later |
| Claude in Slack | Spawns web session from `@Claude` | Starting work from team chat |
| Standard MCP | Claude queries on demand during a task | Read/query systems when needed |
| Remote Control | Drive local session from claude.ai or mobile | Steering without forwarding external events |

Remote Control is you steering from claude.ai or your phone. Channels are the outside world knocking: a webhook, a chat message, an alert from a system that doesn't know you're at your desk.

## Requirements

You need Claude Code **v2.1.80+** (permission relay starts at **v2.1.81+**). Channels are a research preview on Anthropic auth only - no Bedrock, GCP Agent Platform, or Foundry. Official plugins expect **Bun** on the path. Team and Enterprise orgs need an Owner to flip `channelsEnabled`; Pro and Max users without an org skip that check.

## Enable an official channel

Telegram, Discord, and iMessage share the same choreography: install the plugin, drop in credentials, restart with `--channels`, pair your account, lock the allowlist.

**Telegram** (Discord and iMessage mirror this in the docs):

```bash
# In Claude Code
/plugin install telegram@claude-plugins-official
/reload-plugins
/telegram:configure <bot-token-from-botfather>

# Exit and restart with the channel active
claude --channels plugin:telegram@claude-plugins-official

# In Telegram: DM bot → get pairing code
# Back in Claude Code:
/telegram:access pair <code>
/telegram:access policy allowlist
```

Stack plugins on one command: `claude --channels plugin:telegram@... plugin:discord@...`.

Want to poke the pipe without tokens? **fakechat** runs a localhost UI at `http://localhost:8787`. Type there, read the `<channel>` event in your session.

### What you see in the terminal

Inbound messages show up. So does the outbound tool call (`sent`). The actual reply text? That stays on Telegram, Discord, or iMessage - not in the terminal pane.

## Security model

`--channels` is per-session opt-in: you name which servers may inject. On top of that, every approved plugin keeps a sender allowlist; strangers get dropped without an error. Org admins add `channelsEnabled` and can replace Anthropic's plugin list with `allowedChannelPlugins`.

Check the sender, not the room. In group chats, `message.from.id` and `message.chat.id` are different fields. Allowlisting the room ID means anyone in that group can prompt-inject.

Permission relay is the sharp edge. Declare `claude/channel/permission` and anyone who can reply through the channel can approve or deny tool use in your session. Turn relay on only when every allowlisted sender is someone you'd hand the keyboard to.

`.mcp.json` alone doesn't inject. Without `--channels`, the MCP server may connect and expose tools, but channel messages never arrive.

One more cause since Claude Code 2.1.292: a stdio channel server that negotiates MCP protocol 2026-07-28 isn't registered as a channel at all, because that revision can't carry channel messages. `MCP_PROTOCOL_NEGOTIATION=legacy` keeps it on the older handshake - see [MCP connected, zero tools](/articles/claude-code-mcp-2026-07-28).

## Build a one-way webhook channel

Custom servers aren't on the research-preview allowlist. Local testing:

```bash
claude --dangerously-load-development-channels server:webhook
```

Minimal server (Bun + MCP SDK):

```ts
#!/usr/bin/env bun
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'

const mcp = new Server(
  { name: 'webhook', version: '0.0.1' },
  {
    capabilities: { experimental: { 'claude/channel': {} } },
    instructions:
      'Events arrive as <channel source="webhook" ...>. One-way: read and act, no reply.',
  },
)

await mcp.connect(new StdioServerTransport())

Bun.serve({
  port: 8788,
  hostname: '127.0.0.1',
  async fetch(req) {
    const body = await req.text()
    await mcp.notification({
      method: 'notifications/claude/channel',
      params: {
        content: body,
        meta: { path: new URL(req.url).pathname, method: req.method },
      },
    })
    return new Response('ok')
  },
})
```

Register in `.mcp.json`:

```json
{
  "mcpServers": {
    "webhook": { "command": "bun", "args": ["./webhook.ts"] }
  }
}
```

Test:

```bash
curl -X POST localhost:8788 -d "build failed on main: https://ci.example.com/run/1234"
```

Claude receives:

```text
<channel source="webhook" path="/" method="POST">build failed on main: https://ci.example.com/run/1234</channel>
```

`meta` keys become attributes on the `<channel>` tag. Stick to letters, digits, underscores - hyphens vanish silently. And notifications aren't acknowledged: write to the transport succeeds even when Claude never sees the event because the session didn't register your server as a channel.

## Two-way: reply tool and permission relay

Two-way bridges add `tools: {}` and a `reply` handler. Your `instructions` string has to tell Claude which attribute from the inbound tag to pass back (`chat_id`, etc.).

Permission relay (v2.1.81+) adds a second path: declare `claude/channel/permission` and listen for `notifications/claude/channel/permission_request` (a five-letter `request_id`, plus `tool_name`, `description`, `input_preview`). Teach your inbound handler to recognize `yes abcde` / `no abcde` as verdicts on `notifications/claude/channel/permission` - the local terminal prompt stays live too, and whichever answer arrives first wins.

## Unattended sessions

Permission prompts pause the session. Relay-capable channels can forward those prompts to your phone so you're not stuck at the desk. `--dangerously-skip-permissions` is the other exit - unattended only, trusted environment only, and think twice if channels are in the mix.

Running with `-p` (non-interactive) disables anything that needs a terminal choice - multiple-choice questions, plan approval - so the run can't hang waiting for you.

## Enterprise controls

| Setting | Purpose |
| --- | --- |
| `channelsEnabled` | Master switch; Team/Enterprise blocked until Owner enables |
| `allowedChannelPlugins` | Replaces Anthropic allowlist when set; empty array blocks all listed plugins |

Orgs on a Console API key (not Team/Enterprise) get channels on by default - unless the org deploys managed settings, in which case `channelsEnabled: true` must be set there. Either way, each user still passes `--channels` to opt in for that session.

## What to read next

Anthropic's [channels guide](https://code.claude.com/docs/en/channels) covers Telegram/Discord/iMessage setup and org toggles; the [channels reference](https://code.claude.com/docs/en/channels-reference) has the full notification contract, sender gating, and the assembled webhook + relay example. On this site, [Claude Code tools](/articles/claude-code-tools) explains how MCP names land in context at startup; [the agentic loop](/articles/claude-code-agentic-loop-context) covers what happens after an event arrives - including the [startup tax](/notes/startup-tax) those MCP names add before you type.

Sources:
- Push events into a running session with channels: https://code.claude.com/docs/en/channels
- Channels reference: https://code.claude.com/docs/en/channels-reference
- Claude Code changelog - 2.1.80 and 2.1.81: https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md
