# Channels Reference [Beta]

> **Beta Feature**: Channels is currently in Beta. The protocol and interfaces may be adjusted based on feedback.

A Channel is a special MCP server that pushes external events (webhooks, chat messages, monitoring alerts, etc.) into a CodeBuddy Code session, allowing CodeBuddy Code to react to things happening outside the terminal.

## Overview

A Channel runs on the same machine as CodeBuddy Code and communicates via stdio. CodeBuddy Code launches it as a child process.

Typical use cases:

- **Chat platforms** (Telegram, Discord): The plugin runs locally, polls the platform API for new messages, and forwards them to CodeBuddy Code
- **Webhooks** (CI, monitoring): A server listens on a local HTTP port, receives POST requests from external systems, and pushes them to CodeBuddy Code

Channels operate in two modes:

| Mode | Description |
|------|-------------|
| One-way | Only forwards events to CodeBuddy Code (alerts, webhooks), processed in the local session |
| Two-way | Additionally exposes a reply tool, allowing CodeBuddy Code to send responses |

## Using a Channel

### Startup

Use the `--channels` parameter to specify which channels to load:

```bash
# Load a plugin-type channel
codebuddy --channels plugin:fakechat@claude-plugins-official

# Load a channel server configured in .mcp.json
codebuddy --channels server:webhook

# Load multiple channels (comma-separated)
codebuddy --channels plugin:telegram@claude-plugins-official,plugin:discord@claude-plugins-official
```

### Development Mode

Use the `--dangerously-load-development-channels` flag to test custom channels during initial development. This flag allows any channel to run without being on the allowlist:

```bash
codebuddy --dangerously-load-development-channels server:my-webhook
```

This flag only bypasses the allowlist check; the `channelsEnabled` organization policy still applies. Once a channel is submitted to the official marketplace and passes security review, it gets added to the allowlist and can be loaded directly with `--channels`.

### Channel Messages in Sessions

Channel messages are injected into CodeBuddy Code's context as `<channel>` tags:

```xml
<channel source="fakechat" sender="web" chat_id="1">Hello, can you help me look at this issue</channel>
```

In the TUI, channel messages are displayed in a friendly format:

```
#fakechat · web: Hello, can you help me look at this issue
```

### Settings

Channel functionality can be controlled in `settings.json`:

```json
{
  "channelsEnabled": true
}
```

Set to `false` to completely disable channel functionality.

## Building a Channel

### Basic Requirements

A channel server needs to:

1. Declare the `claude/channel` capability so CodeBuddy Code registers a notification listener
2. Send `notifications/claude/channel` events
3. Connect via stdio transport (CodeBuddy Code launches it as a child process)

### Minimal Example: Webhook Receiver

```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 from webhook arrive as <channel source="webhook" ...> tags. One-way channel, just read and process.',
  },
)

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"] }
  }
}
```

## Server Configuration Options

| Field | Type | Description |
|-------|------|-------------|
| `capabilities.experimental['claude/channel']` | `object` | **Required**. Always `{}`. Once declared, CodeBuddy Code registers a notification listener |
| `capabilities.experimental['claude/channel/permission']` | `object` | Optional. Always `{}`. Declares that this channel can receive permission relay requests |
| `capabilities.tools` | `object` | Required for two-way channels. Always `{}`. Standard MCP tool capability |
| `instructions` | `string` | Recommended. Injected into the system prompt, telling CodeBuddy Code how to handle events from this channel |

## Notification Format

When sending `notifications/claude/channel` notifications, params contains two fields:

| Field | Type | Description |
|-------|------|-------------|
| `content` | `string` | Event content, becomes the body of the `<channel>` tag |
| `meta` | `Record<string, string>` | Optional. Each key-value pair becomes an attribute of the `<channel>` tag. Key names only allow letters, numbers, and underscores; keys containing hyphens or other characters are silently discarded |

Example:

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

Format when arriving at CodeBuddy Code:

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

## Exposing a Reply Tool

Two-way channels need to expose standard MCP tools for CodeBuddy Code to reply to messages:

```ts
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'

mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'reply',
    description: 'Send a reply message through this channel',
    inputSchema: {
      type: 'object',
      properties: {
        chat_id: { type: 'string', description: 'The conversation ID to reply to' },
        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 }
    // Call your chat platform API to send the message
    await sendToPlatform(chat_id, text)
    return { content: [{ type: 'text', text: 'sent' }] }
  }
  throw new Error(`unknown tool: ${req.params.name}`)
})
```

## Sender Security Controls

An unprotected channel is a prompt injection attack vector. Anyone who can reach your endpoint can inject text into CodeBuddy Code.

Before calling `mcp.notification()`, you must verify the sender's identity:

```ts
const allowed = new Set(loadAllowlist())

// In the message handler, before sending the notification:
if (!allowed.has(message.from.id)) {  // Check sender ID, not group ID
  return  // Silently discard
}
await mcp.notification({ ... })
```

**Important**: Always validate based on sender identity (`message.from.id`) rather than chat room identity (`message.chat.id`). In group chats, these two values differ, and validating by group would allow anyone in the group to inject messages into the session.

## Permission Relay

When CodeBuddy Code calls a tool that requires approval (such as `Bash`, `Write`, `Edit`), the local terminal opens a permission dialog. Two-way channels can optionally receive this prompt as well, allowing you to approve or deny from a remote device.

Both the local terminal and remote channel dialogs open simultaneously. **The first response wins**, and the other is automatically dismissed.

### Enabling Permission Relay

Add `claude/channel/permission` in the Server constructor:

```ts
capabilities: {
  experimental: {
    'claude/channel': {},
    'claude/channel/permission': {},  // Enable permission relay
  },
  tools: {},
},
```

### Permission Request Fields

CodeBuddy Code sends `notifications/claude/channel/permission_request` with the following fields:

| Field | Description |
|-------|-------------|
| `request_id` | 5 lowercase letters (excluding `l`), used to match replies |
| `tool_name` | The tool CodeBuddy Code wants to use, e.g., `Bash`, `Write` |
| `description` | Human-readable description of the tool call |
| `input_preview` | JSON string of tool parameters, truncated to 200 characters |

### Sending a Verdict

Your channel needs to send a `notifications/claude/channel/permission` notification:

```ts
await mcp.notification({
  method: 'notifications/claude/channel/permission',
  params: {
    request_id: '<received request_id>',
    behavior: 'allow',  // or 'deny'
  },
})
```

### Handling Inbound Verdicts

In your inbound message handler, recognize replies in the format `yes <id>` or `no <id>`:

```ts
// Match "y abcde", "yes abcde", "n abcde", "no abcde"
// [a-km-z] is the ID alphabet used by CodeBuddy Code (lowercase, skipping 'l')
const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i

const m = PERMISSION_REPLY_RE.exec(message.text)
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 a verdict, not forwarded as chat
}
```

## Packaging as a Plugin

Wrapping a channel as a plugin makes it easy to share and install. Users install via `/plugin install`, then enable with `--channels plugin:<name>@<marketplace>`.

## References

- [fakechat example](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/fakechat): A complete two-way channel implementation with Web UI, file attachments, and reply tool
- [MCP Protocol](https://modelcontextprotocol.io): Channels are built on the MCP protocol
