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):
# 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.
Build a one-way webhook channel
Custom servers aren’t on the research-preview allowlist. Local testing:
claude --dangerously-load-development-channels server:webhook
Minimal server (Bun + MCP SDK):
#!/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:
{
"mcpServers": {
"webhook": { "command": "bun", "args": ["./webhook.ts"] }
}
}
Test:
curl -X POST localhost:8788 -d "build failed on main: https://ci.example.com/run/1234"
Claude receives:
<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 managed settings say otherwise. Either way, each user still passes --channels to opt in for that session.
What to read next
Anthropic’s channels guide covers Telegram/Discord/iMessage setup and org toggles; the channels reference has the full notification contract, sender gating, and the assembled webhook + relay example. On this site, Claude Code tools explains how MCP names land in context at startup; the agentic loop covers what happens after an event arrives.