Human-in-the-Loop Agent Task Management for Claude Code
Back to Blog
March 25, 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. 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 tag and injects it into Claude's context:

text

build failed on main: https://ci.example.com/run/1234

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:

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 . 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:

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

build failed on main

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:

Then update instructions to tell Claude when and how to use the reply tool, and which 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 or no 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:

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 a claude.ai login. Console and API key authentication are not supported. Team and Enterprise organizations need to explicitly enable channels before they can be used.

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:

To have your channel added to the allowlist, submit it to the official marketplace. Channels go through security review before approval. On Team and Enterprise plans, admins can also add plugins to their organization's own allowlist.

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