Human-in-the-Loop Task Manager for AI Agents
Back to Blog
March 25, 2026 | Updated October 2, 2026 | AgentRQ Team

How Claude Code Channels Work

Claude Code v2.1.80 introduced Channels in research preview — a capability that lets you push events into a running Claude Code session from the outside world.

This post is kept up to date. Jump to the flags and settings reference or the latest changes if that is what you came for. A CI build failure, a Telegram message, a monitoring alert: any of these can now wake Claude up and trigger action, without you having to type a thing.

AgentRQ has been building on this pattern since before it had an official name. Here's how channels work under the hood, and how to build your own.

What Is a Channel?

A channel is an MCP server that pushes events into Claude Code rather than only responding to tool calls from it. Claude Code spawns your channel as a subprocess and communicates over stdio — standard MCP. The channel-specific part is declaring the claude/channel experimental capability in the Server constructor, which registers a notification listener inside Claude Code.

Once registered, your server can call mcp.notification() at any time with method notifications/claude/channel. Claude Code wraps the payload in a <channel> tag and injects it into Claude's context:

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

Claude sees this tag, understands what happened from the source and attributes, and takes action — all without you typing anything.

One-Way vs. Two-Way

Channels come in two flavors:

  • → One-way: your server pushes events, Claude acts in the session. Good for CI alerts, monitoring webhooks, or any "fire and forget" notification.
  • → Two-way: your server also exposes a reply MCP tool that Claude can call to send messages back. Good for chat bridges (Telegram, Discord, iMessage).

A two-way channel can also opt into permission relay: when Claude needs to approve a tool call, the prompt can be forwarded to your channel so you can approve or deny it remotely — from your phone, for example.

The Minimum Viable Channel

Three things are required:

  1. Declare capabilities.experimental['claude/channel']: {} in your Server constructor
  2. Emit notifications/claude/channel notifications when events arrive
  3. Connect over stdio (Claude Code spawns your server as a subprocess)

Here's a complete one-way webhook receiver in about 30 lines:

typescript
#!/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" ...>. Read and act — no reply expected.',
  },
)

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 it in .mcp.json:

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

Start Claude Code with the development flag (required during the research preview, since custom channels aren't on the approved allowlist yet):

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

Send it a test payload:

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

Claude receives it and acts. No manual intervention needed.

Notification Format

Your server pushes events by calling mcp.notification() with two params:

  • → content — the event body, delivered as the text inside the <channel> tag.
  • → meta — optional key-value pairs, each becoming an attribute on the tag. Keys must use letters, digits, and underscores only.

The source attribute is set automatically from your server's configured name. So this notification:

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

Arrives in Claude's context as:

text
<channel source="webhook" severity="high" run_id="1234">
build failed on main
</channel>

The instructions string in your Server constructor goes into Claude's system prompt. Use it to tell Claude what events to expect, what the attributes mean, whether to reply, and if so which tool to use.

Adding Replies (Two-Way Channels)

To make a channel two-way, add tools: {} to capabilities and register two MCP request handlers:

  • → ListToolsRequestSchema — describes your reply tool so Claude knows it exists
  • → CallToolRequestSchema — implements the send logic when Claude calls it

Then update instructions to tell Claude when and how to use the reply tool, and which <channel> attribute to pass back as the chat_id. Here's the key addition:

typescript
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'reply',
    description: 'Send a message back over this channel',
    inputSchema: {
      type: 'object',
      properties: {
        chat_id: { type: 'string', description: 'The conversation to reply in' },
        text: { type: 'string', description: 'The message to send' },
      },
      required: ['chat_id', 'text'],
    },
  }],
}))

mcp.setRequestHandler(CallToolRequestSchema, async req => {
  if (req.params.name === 'reply') {
    const { chat_id, text } = req.params.arguments as { chat_id: string; text: string }
    // send to your platform here
    return { content: [{ type: 'text', text: 'sent' }] }
  }
  throw new Error(`unknown tool: ${req.params.name}`)
})

Sender Gating

An ungated channel is a prompt injection vector — anyone who can POST to your endpoint can put arbitrary text in front of Claude. Always gate on sender identity before emitting:

typescript
const allowed = new Set(loadAllowlist())

if (!allowed.has(message.from.id)) {
  return  // drop silently
}
await mcp.notification({ ... })

One important detail: gate on sender identity (message.from.id), not room identity (message.chat.id). In group chats these differ — gating on the room would let anyone in an allowlisted group inject prompts into your session.

The official Telegram and Discord channels use a pairing flow: the user DMs the bot, the bot replies with a code, the user approves it in their Claude Code session, and their platform ID gets added to the allowlist.

Permission Relay

Two-way channels can forward tool-approval prompts to you remotely. When Claude wants to run Bash or Write, the approval dialog can appear in your chat app — not just the local terminal. Both stay live: the first answer wins.

To opt in, add claude/channel/permission: {} under experimental capabilities. Then:

  1. Handle notifications/claude/channel/permission_request — format the prompt and send it through your platform API, including the request_id
  2. In your inbound handler, check for replies matching yes <id> or no <id> and emit a notifications/claude/channel/permission verdict

The regex for matching verdicts is /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i. The five-letter ID alphabet skips l so it's never confused with 1 when typed on a phone.

Here's the verdict emitter:

typescript
const m = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i.exec(inboundText)
if (m) {
  await mcp.notification({
    method: 'notifications/claude/channel/permission',
    params: {
      request_id: m[2].toLowerCase(),
      behavior: m[1].toLowerCase().startsWith('y') ? 'allow' : 'deny',
    },
  })
  return  // handled as verdict — don't also forward as chat
}

How AgentRQ Fits In

AgentRQ implements a production-grade version of this exact pattern. When Claude Code connects to AgentRQ via MCP, it gets:

  • → Inbound tasks from humans delivered via channel notifications
  • → Reply and createTask tools for bidirectional communication
  • → Human-in-the-loop approvals for reviewing Claude's planned actions
  • → Attachment support for sharing files between Claude and humans

The key difference: AgentRQ routes messages through a hosted service so you don't have to run a local server or manage an allowlist. You receive and respond to tasks from the AgentRQ app on any device.

This means you can have full human-in-the-loop oversight of Claude Code sessions from anywhere — while Claude keeps working on your machine.

Requirements and Limitations

Channels require Claude Code v2.1.80 or later and Anthropic authentication: either a claude.ai login or, since v2.1.128, a Console API key. They are not available through Amazon Bedrock, Google Cloud or Microsoft Foundry. The official channel plugins need Bun installed. Team and Enterprise organizations must enable channels before anyone can use them (see the reference below).

During the research preview, your custom channel won't be on the approved allowlist, so you need the development flag:

bash
claude --dangerously-load-development-channels server:<name>

To have your channel added to the allowlist, submit it to the official marketplace. Channels go through security review before approval. Organizations can also maintain their own allowlist with the allowedChannelPlugins managed setting.

Claude Code Channels Reference

Every flag and setting people ask about, checked against the Claude Code documentation and changelog on October 2, 2026.

Status

Channels are still a research preview as of Claude Code 2.1.287 (October 1, 2026). Anthropic says the flag syntax and protocol may change. Neither --channels nor the development flag appears in claude --help during the preview.

The channels flag

bash
claude --channels plugin:telegram@claude-plugins-official

--channels takes a space-separated list of plugin:<name>@<marketplace> entries and turns on channel notifications from those servers for this one session. Having a server in .mcp.json is not enough: it only pushes messages into the session if it is also named here. Each entry must be on Anthropic's allowlist, or on your organization's allowedChannelPlugins list. If it is not, Claude Code still starts, the channel just doesn't register, and a notice at startup says why. Since v2.1.281 the plugin name has to match the allowlist entry, not only the marketplace.

The dangerously-load-development-channels flag

bash
claude --dangerously-load-development-channels server:webhook
claude --dangerously-load-development-channels plugin:mychannel@my-marketplace

This flag exists because custom channels are not on the allowlist during the preview. It accepts server:<name> for a plain server in your .mcp.json, or a plugin: entry. It skips the allowlist check only, for each entry you list, after a confirmation prompt. It does not extend to entries passed with --channels, and it does not get around an organization that has channels turned off. This is the flag AgentRQ uses: claude --dangerously-load-development-channels server:agentrq-<workspace>.

Who can use channels

Account Channels
Pro or Max, no organization Available, no admin step
claude.ai Team or Enterprise Off until an Owner enables it in Admin settings → Claude Code → Channels, or sets the managed channelsEnabled: true
Console (API key) Available since v2.1.128, unless the organization deploys managed settings, which then need channelsEnabled: true
Bedrock, Google Cloud, Foundry Not available

allowedChannelPlugins is a managed setting that replaces Anthropic's default allowlist for an organization. An empty list blocks every channel plugin except those loaded with the development flag.

Official channels: Telegram, Discord and iMessage

Anthropic publishes three channels in the claude-plugins-official marketplace, plus a fakechat demo that runs on localhost:

  • → Telegram: /plugin install telegram@claude-plugins-official, then /telegram:configure <bot token>, restart with --channels plugin:telegram@claude-plugins-official, and pair your account with /telegram:access pair <code>.
  • → Discord: the same steps with discord in place of telegram.
  • → iMessage: macOS only and needs no token. Messages you send to yourself are let through automatically, and /imessage:access allow <handle> adds other senders.

If the marketplace is missing, add it first with /plugin marketplace add anthropics/claude-plugins-official.

The MCP side

A channel server declares capabilities.experimental["claude/channel"] and sends notifications/claude/channel with a content string and optional meta. Each meta key becomes an attribute on the <channel> tag Claude sees, and keys may only use letters, digits and underscores. To relay permission prompts, the server also declares experimental["claude/channel/permission"]. Claude Code then sends notifications/claude/channel/permission_request, and the server answers with notifications/claude/channel/permission carrying allow or deny. The terminal prompt stays open too, and whichever answer arrives first wins.

preferredNotifChannel is not a channel

People often find preferredNotifChannel while searching for channels. It is unrelated: it decides how Claude Code tells you on your own machine that a task finished or is waiting for permission. Values are auto (the default), terminal_bell, iterm2, iterm2_with_bell, kitty, ghostty and notifications_disabled. Set it in /config under local notifications, or in ~/.claude/settings.json:

json
{ "preferredNotifChannel": "terminal_bell" }

Older guides say to run claude config set --global preferredNotifChannel terminal_bell. The claude config command was removed in Claude Code 2.0, so that no longer works.

Claude Code Channels Latest Changes

Last checked October 2, 2026. Dates are npm release dates.

Version Date Change
2.1.281 2026-09-23 --channels plugin entries must match the plugin name, not just the marketplace
2.1.267 2026-09-09 An unreadable managed allowedChannelPlugins now admits nothing; string entries like "plugin@marketplace" accepted
2.1.234 2026-08-17 Permission previews go only to channel servers that passed the sender gate; an explicit opt-out is honored
2.1.211 2026-07-15 Relayed permission previews strip hidden and look-alike characters
2.1.187 2026-06-23 Channel connections no longer drop after the agents view, /bg, /tui or /update
2.1.128 2026-05-04 Channels work with Console API keys; Console orgs with managed settings need channelsEnabled: true
2.1.126 2026-04-30 Plan mode tools work again in interactive --channels sessions
2.1.105 2026-04-13 Team and Enterprise: inbound messages no longer dropped after the first one
2.1.84 2026-03-25 allowedChannelPlugins managed setting added
2.1.83 2026-03-24 Fixed "Channels are not currently available" after upgrading
2.1.81 2026-03-20 Permission relay over channels added
2.1.80 2026-03-19 --channels added as a research preview

Next Steps

The official Claude Code channel implementations for Telegram, Discord, and iMessage are the best reference for production patterns: pairing flows, file attachment handling, and full permission relay.

If you'd rather skip the infrastructure and get straight to human-in-the-loop collaboration, AgentRQ connects Claude Code to you in 60 seconds — no server to run.

Start Free