architecture
Claude Channels: Deep Dive
How Claude Code routes work across channels. Slack, GitHub, Linear, terminal: the surfaces that make Claude omnipresent.
Architecture · 9 min
Claude Channels: Deep Dive
How Claude Code routes work across channels. Slack, GitHub, Linear, terminal: the surfaces that make Claude omnipresent.
Claude Code Channels make the model omnipresent: terminal, Slack, GitHub, Linear, Telegram, or a web client. Every surface follows the same routing model: a channel plugin runs as an MCP server on your machine, polls or listens for incoming messages, and pushes events into Claude's idle loop. Nothing lives in the cloud. Nothing requires a relay. The channel is just a text bridge.
The Full Picture
Your Phone (Telegram / Slack / Linear)
|
| HTTPS
v
External Platform (api.telegram.org / slack.com / …)
|
| Bot API polling / webhook
v
Channel Plugin (MCP server on your Mac)
|
| pushes event
v
Claude Code Session (your Mac)
|── files, git, MCP tools, skills, bash
|
| calls reply tool
v
Channel Plugin ──> Platform ──> Your PhoneChannel Model Overview
A channel plugin is an MCP server. It registers tools with Claude (e.g. send_telegram_message) and pushes incoming messages as events. Claude treats channel tools exactly like Attio, Notion, or any other MCP server: just callable tools. The distinction is directionality: regular MCP is pull-based (Claude decides when to call), channels are push-based (the plugin fires events into Claude's idle loop).
Regular MCP: Pull
- · Claude decides when to call the tool
- · Server waits passively
- · You type "check Attio" in terminal
- · Claude calls the Attio MCP tool
Channel MCP: Push
- · Plugin pushes events TO Claude
- · Plugin actively polls for messages
- · You text from your phone
- · Plugin pushes event; Claude reacts
The 6 Channels
Each channel implements the same MCP interface with a different transport.
The default channel. You type directly into the Claude Code REPL. No plugin, no polling; input arrives synchronously. Every other channel wraps this primitive.
> check Attio for open deals
The Slack MCP plugin connects via the Slack Bolt SDK and a bot token. Messages in a designated channel or DM are polled (or received via socket mode). Responses are posted back usingchat.postMessage. Ideal for team commands that don't require terminal access.
GitHub channels trigger on PR events via a webhook or the GitHub App API. Claude receives the diff, runs its review skill, and posts comments directly on the PR. Can also respond to@claude review mentions in PR comments.
The Linear channel polls for issues assigned to a bot user or labeled withclaude. When one is found, Claude reads the issue body, executes the task (write code, open a PR, update docs), and comments the result back on the Linear issue.
The Telegram plugin pollsapi.telegram.org/getUpdatesevery 2-3 seconds via a bot token. Incoming messages are checked against an allowlist (paired via a 5-letter code flow). Approved messages become events in Claude's idle loop. Replies go back viasendMessage. Permission prompts are forwarded to your phone for yes/no approval.
A local HTTP server (or Next.js app) connects to the Claude Code session via the Chat SDK. The browser sends prompts and streams responses over a WebSocket or SSE connection. The web channel is the surface for custom dashboards and cockpit UIs.
Routing Rules
Every message, regardless of channel, passes through the same routing logic before it reaches Claude's model context.
- 1
Allowlist check
Sender identity is validated against the channel's allowlist. Messages from unknown senders are silently dropped: no error, no acknowledgement.
- 2
Event construction
The plugin wraps the raw message in a channel event:
{ type: "channel_message", text, reply_tool, chat_id } - 3
Idle loop injection
The event is pushed into Claude's event queue. Claude picks it up during its idle state, when it has finished its current task and is waiting for input. If Claude is busy, the event waits in the queue.
- 4
Permission relay (if needed)
When Claude needs approval for a destructive action, the permission prompt fires simultaneously in the terminal and in the originating channel. The first answer (yes/no + request ID) wins; the second is discarded.
- 5
Reply dispatch
Claude calls the reply tool registered by the channel plugin (e.g.
send_telegram_message). The plugin forwards the response back to the originating surface.
No queue persistence
Messages sent while the Claude Code session is not running are lost. The idle loop only exists while the process is alive. Combineclaude --channels with a launchd KeepAlive entry to auto-restart the session on crash and keep the bot available.State Sync
Claude Code sessions maintain a single shared context. When a Telegram message arrives, Claude has the same CLAUDE.md, the same files, the same open MCP connections as if you typed the prompt in the terminal. There is no per-channel session isolation; all channels share one model context.
Shared
- CLAUDE.md context
- Open files & git state
- Active MCP connections
- Skill registry
- Conversation history
Per-channel
- Reply tool reference
- Chat / channel ID
- Sender identity
- Allowlist config
- Permission request IDs
Not synced
- Queued messages (offline)
- Cross-session history
- Channel-to-channel routing
Real Example: PR Review Across Channels
A PR is opened on GitHub. Claude reviews it, posts inline comments, and notifies Slack. The flow touches three channels sequentially.
GitHub webhook delivers a pull_request event to the GitHub channel plugin. Plugin checks: is the repo in the allowlist? Yes. Constructs channel event with diff payload.
{ type: "channel_message",
text: "PR #42 opened by @dev: feat/new-endpoint",
reply_tool: "post_github_review",
context: { pr_number: 42, diff_url: "…" } }Event lands in the idle loop. Claude reads CLAUDE.md, fetches the diff via the GitHub MCP, runs the /review skill, checks for security issues with /audit.
Read: .github/CODEOWNERS
MCP: mcp__github__get-pull-request-diff { pr: 42 }
Skill: /review → 3 issues found
Skill: /audit → no critical vulnerabilitiesClaude calls the reply tool with structured review comments. Plugin posts them as GitHub review comments on the PR diff.
post_github_review({
pr_number: 42,
comments: [
{ path: "api/endpoint.ts", line: 34,
body: "Missing input validation on userId" }
],
event: "REQUEST_CHANGES"
})Claude calls the Slack reply tool to post a summary in #engineering. The Slack plugin POSTs to chat.postMessage.
send_slack_message({
channel: "#engineering",
text: "PR #42 reviewed: 3 comments, changes requested."
})What stays on your machine
Only the text of the review comments and the Slack message leave your Mac. The diff, your codebase, credentials, and all tool data stay local. Channels are text bridges, nothing more.
claude -p vs claude --channels
claude -p
- · One-shot execution: run, print, exit
- · Used by /scheduler and launchd cron jobs
- · No event loop, no idle state
- · Cannot receive Channel messages
- · Perfect for scheduled, headless tasks
claude --channels
- · Long-running session with event loop
- · Starts channel plugins as MCP servers
- · Listens for terminal AND channel input
- · Stays alive until you close it
- · Pair with launchd KeepAlive for 24/7 bot
Want this running in your stack?
AY Automate builds agent systems, RAG pipelines, and Claude Code setups for production teams.