Claude Code Channels: What They Are and What You Can Build
Claude Code v2.1.80 shipped channels. v2.1.81 added permission relay. Together they're the most interesting developer-facing addition to Claude Code since hooks.
This is what they actually are, how the API works, and what you can build with them — including a note on how I was doing this manually before they existed.
What a Channel Is
A channel is an MCP server with a specific capability declared:
capabilities: {
experimental: { 'claude/channel': {} }
}
That single entry is what makes it a channel rather than a regular MCP server. Claude Code sees it, registers a notification listener, and now your server can push events into the running session.
You emit events with mcp.notification():
await mcp.notification({
method: 'notifications/claude/channel',
params: {
content: 'build failed on main',
meta: { severity: 'high', run_id: '1234' },
},
})
Claude receives this wrapped in a <channel> tag:
<channel source="your-server" severity="high" run_id="1234">
build failed on main
</channel>
Claude sees it in context and acts. No polling. No extra process. Claude Code spawns your channel server as a subprocess over stdio and handles the plumbing.
One-Way vs. Two-Way
A one-way channel pushes events in and Claude acts on them. Useful for: CI failure alerts, monitoring, anything where you want Claude to react to something happening outside the terminal.
A two-way channel adds a reply tool so Claude can send responses back. You register a standard MCP tool called reply (or whatever you name it), and Claude calls it when it wants to respond. The tool sends the message back through your channel — to your chat app, to a web UI, wherever.
// Claude calls this to send messages back
mcp.setRequestHandler(CallToolRequestSchema, async req => {
if (req.params.name === 'reply') {
const { chat_id, text } = req.params.arguments
// post to your chat platform, push to your web UI, etc.
send(text)
return { content: [{ type: 'text', text: 'sent' }] }
}
})
This is the pattern for a chat bridge: Telegram, Discord, a web UI. Messages come in as channel events, Claude processes and replies via the tool.
Permission Relay
This is the part I find most useful.
When Claude wants to run a tool that needs approval — Bash, Write, Edit — the local terminal shows a prompt and blocks. Permission relay sends the same prompt through your channel simultaneously, so you can approve or deny from wherever your channel delivers it. Both paths stay live; whichever answer arrives first wins.
To opt in, declare an additional capability:
capabilities: {
experimental: {
'claude/channel': {},
'claude/channel/permission': {}, // opt in to permission relay
}
}
Claude Code sends notifications/claude/channel/permission_request with four fields:
request_id— five lowercase letters, include this in your outgoing prompttool_name— e.g.Bash,Writedescription— what this specific call doesinput_preview— the tool arguments, truncated to 200 chars
Your server formats and forwards the prompt. The remote user replies with yes <id> or no <id>. Your server parses it and emits the verdict:
await mcp.notification({
method: 'notifications/claude/channel/permission',
params: {
request_id: 'abcde',
behavior: 'allow', // or 'deny'
},
})
That's it. Claude Code matches the ID, applies the verdict, and continues (or stops).
Requires v2.1.81+. Requires claude.ai login — Console and API key auth aren't supported yet during the research preview.
Before Channels Existed
Before channels shipped, I was doing this with Claude Code hooks and tmux.
The PermissionRequest hook fires a script when Claude needs approval. My script parsed the hook payload, sent a Telegram message with Approve/Deny buttons, waited for the callback, then ran tmux send-keys y Enter or tmux send-keys n Enter to the right pane.
It worked. It required: detecting which tmux pane belonged to which session, validating targets to prevent injection, stripping ANSI from pane output, handling numbered option extraction, deduplicating notifications, managing state files across hook invocations.
The native permission relay does all of this in about 15 lines. The hook approach will still work — it fires before Claude Code sends the relay notification, and the relay has a 1.5s fallback — but for new implementations the channel API is the right path.
What I Built With It
AgentHive uses channels as the bridge for Claude Code sessions. A channel.ts MCP server joins an AgentHive room when you start a CC session. Messages from the room reach Claude as channel events. Claude replies via the reply tool. Permission requests appear as approval cards in the room feed.
The practical result: I run Claude Code in a terminal, open AgentHive on any device, see the session feed in real time, approve or deny tool calls without being at the terminal. The native relay replaced the tmux pane send approach when v2.1.81 shipped.
What You Can Build
The pattern is simple enough that the interesting work is in what you connect it to:
Chat bridge: Telegram, Discord, Slack. Bidirectional — send instructions, get updates, approve permissions from your phone.
CI integration: Pipe build failures or test results into a running session. Claude investigates and fixes autonomously, asks for approval before pushing.
Monitoring: Route alerts from Datadog, PagerDuty, or whatever you're watching into a session. Claude triages and responds.
Mobile permission relay: Approve tool calls from your phone. The five-letter ID format is intentionally short for phone keyboards.
Custom control plane: A web UI with a feed of events, approval cards, session status. This is what AgentHive does.
Caveats
Channels are in research preview. A few things to know:
- Requires
claude.ailogin (not Console or API key) - Custom channels need
--dangerously-load-development-channels server:<name>to bypass the allowlist during the preview - Team and Enterprise orgs need to explicitly enable channels in org settings
- Project trust dialogs and MCP server consent don't relay — only tool-use approvals
The development flag requirement means custom channels aren't zero-friction to distribute yet. The Telegram and Discord plugins from Anthropic are on the approved allowlist, so those work without the flag.
Starting Point
The channels reference docs are thorough. If you want a starting point, Anthropic's fakechat server is a complete two-way channel with file attachments and a web UI. The Telegram and Discord plugins show the full pairing flow.
The core is straightforward: one capability declaration, one notification method, one reply tool. The rest is what you connect it to.
Context on where this fits in the broader pattern: The Messaging-Native Agent Moment — the origin story and what it all points at. Building AgentHive — what I built on top of this API.