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-officialDevelopment 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-webhookThis 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 issueSettings โ
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:
- Declare the
claude/channelcapability so CodeBuddy Code registers a notification listener - Send
notifications/claude/channelevents - 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: A complete two-way channel implementation with Web UI, file attachments, and reply tool
- MCP Protocol: Channels are built on the MCP protocol