architecture

Claude Channels: Deep Dive

How Claude Code routes work across channels. Slack, GitHub, Linear, terminal: the surfaces that make Claude omnipresent.

9 min read·

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 Phone

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

TerminalThe Native Surface

The default channel. You type directly into the Claude Code REPL. No plugin, no polling; input arrives synchronously. Every other channel wraps this primitive.

$ claude
> check Attio for open deals
SlackTeam-Facing Agent

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.

GitHubCode Review Automation

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.

LinearIssue-Driven Execution

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.

TelegramMobile Remote Control

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.

WebBrowser Client

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

    Allowlist check

    Sender identity is validated against the channel's allowlist. Messages from unknown senders are silently dropped: no error, no acknowledgement.

  2. 2

    Event construction

    The plugin wraps the raw message in a channel event:{ type: "channel_message", text, reply_tool, chat_id }

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

1
GitHubPR opened: event fires

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: "…" } }
2
TerminalClaude processes in shared context

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 vulnerabilities
3
GitHubInline comments posted

Claude 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"
})
4
SlackTeam notified

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
Claude channelsmulti-surfaceChat SDKClaude Code integrations

Want this running in your stack?

AY Automate builds agent systems, RAG pipelines, and Claude Code setups for production teams.