ADR-0013: Push Delivery via Claude Code Channels (notify layer over the durable todo queue)
Status · accepted · Date · 2026-07-05 · Deciders · Joe Stump
Context and Problem Statement
ADR-0007 chose a durable, pull-based todo primitive, and one of its stated reasons was that "MCP has no server→client push," so agents drain their list on a worker loop. That is true of the MCP request/response tool model — but it is not the whole story. Claude Code Channels is an open extension built directly on MCP that adds exactly a server→client push: a channel is an MCP server that declares the claude/channel experimental capability, and Claude Code (which spawns it as a stdio subprocess) registers a listener; the server pushes events by emitting a notifications/claude/channel JSON-RPC notification, which arrives in the live session wrapped in a <channel source="…">…</channel> tag.
This gives switchboard a real push path into a live Claude Code session — and, because it is defined on MCP, into other harnesses that implement the same capability. It also unifies the two "switchboard" efforts: a webhook receiver + durable queue and a webhook-to-session channel relay are one system, not two. The question this ADR answers: how do Channels and the durable pull-based todo queue relate — does push become the delivery mechanism, and does adopting it overturn ADR-0007?
Decision Drivers
- Durability must survive an offline session. The Channels reference is explicit that delivery is best-effort and unacknowledged:
notifications/claude/channelresolves when written to the transport, not when Claude processes it, and "if the session hasn't loaded your server as a channel, or the organization policy blocks it, events are dropped silently with no error." Push therefore cannot be the system of record. - Remove polling latency when a session is live. When a harness is attached, work should arrive immediately, not on the next poll tick.
- Reuse the vended endpoint. A channel is an MCP server; the per-agent vended MCP endpoint (ADR-0008) is the natural place to carry the
claude/channelcapability — one transport, not a bolt-on. - Keep attribution + injection safety. The reference warns an ungated channel is a prompt-injection vector and to gate on sender identity. Switchboard already verifies every inbound event per source (ADR-0003) and attributes it to a human (ADR-0008) before it becomes a todo — that verification is the gate.
- Two-way is available and maps onto our model. Channels supports a reply tool (Claude → back over the channel) and permission relay (
claude/channel/permission: Claude Code forwards tool-approval prompts to the channel, which returns an allow/deny verdict). These map onto todo completion and human consent (friend approvals, ADR-0010). - Open + multi-harness. Built on MCP; not a Claude-Code-only lock-in.
Considered Options
- (A) Channels as the system of record — push the event straight into the session and treat that as delivery (the raw-relay prototype pattern). No durable queue.
- (B) Pull-only — keep ADR-0007 exactly as written; ignore the now-available push path.
- (C) Channels as a push-notify layer over the durable todo queue — the todo remains the ledger; when a todo is created/assigned to a consumer whose harness is attached, switchboard emits a
notifications/claude/channelwake carrying the todo id + a summary; the agent still claims/completes via the durable queue. Offline session ⇒ the todo waits to be pulled; nothing is lost. (chosen)
Decision Outcome
Chosen option: "(C) Channels as a notify layer over the durable queue." This revises the framing of ADR-0007 — push is possible now, via Channels — without overturning its decision: durability still requires the queue; Channels is a best-effort ergonomics layer on top, never the ledger.
Push tells the agent "you have work." The durable todo is the work. Delivery is the queue; Channels is the doorbell.
Mechanics (grounded in the standard)
- The vended endpoint speaks
claude/channel. The same MCP endpoint that exposes the todo/webhook/friending verbs (agent-mcp-tools spec) advertisescapabilities.experimental['claude/channel'] = {}. The only open detail is transport, and it is language-independent: if Channels can use the Streamable HTTP MCP transport, switchboard serves channels directly over HTTP alongside its vended endpoints and the web UI's SSE — no separate process; if Channels remains a local stdio subprocess of the harness, a thin Go stdio adapter bridges to central switchboard with the vended credential (ADR-0008). Either way it is the same Go binary/toolchain, and the durable queue stays the ledger (see channel-delivery spec and Open questions). - Notify shape. On todo create/assign to a channel-attached consumer, emit
notifications/claude/channelwithcontent= a legible one-line summary (e.g. "switchboard: todo #4213 onreviews— PR #482 opened in joestump/switchboard") andmeta= routing identifiers (todo_id,queue,kind,source). Per the standard,metakeys must be identifiers (letters/digits/underscore) — hyphenated keys are silently dropped — so switchboard uses snake_case keys. - Lossy by design ⇒ degrade to pull. Because Channels drops events when no session is attached, the todo simply stays
pendingand the worker loop (ADR-0007) drains it when the harness returns. Push never gates correctness. Notify is at-least-once; duplicates are harmless (idempotency-key dedup + idempotentclaim). - Two-way (optional). Expose a reply tool so a chat-sourced todo can be answered inline, and/or opt into permission relay so a human can approve/deny a consent prompt — e.g. a friend approval (ADR-0010) or a scoped tool use — from their own channel, applying the first verdict to arrive. Consent still lands as a durable approval-todo; relay is the fast path, not a bypass of human vending.
- Attribution is the sender gate. Only verified, human-attributed todos are ever pushed; switchboard's per-source verification (ADR-0003) satisfies the reference's "gate on sender" requirement. The prototype's hardening — rejecting payloads containing a
</channel>break, and withholding secret values behind a fetchablesecret-ref— is retained. - Symmetry with the human UI. Humans are pushed via the web UI's SSE (ADR-0001); agents/harnesses are pushed via Channels. Two audiences, two push transports, one durable ledger (ADR-0002).
Consequences
- Good, because it delivers push ergonomics (no polling lag) and preserves ADR-0007 durability — the lossy transport is backed by the durable queue, so an offline session loses nothing.
- Good, because it reuses the vended MCP endpoint as the channel — one transport, one credential, one scope.
- Good, because it unifies the receiver/queue and the channel relay into a single system and product.
- Good, because two-way reply + permission relay give a clean path for inline replies and remote human consent that still route through durable todos.
- Good, because it is MCP-based and multi-harness; harnesses without Channels simply fall back to pull.
- Bad, because switchboard must run a per-harness local channel adapter and track session liveness — accepted; the adapter is thin and the fallback is automatic.
- Bad, because Channels is a research preview with real gates (Claude Code v2.1.80+, permission relay v2.1.81+, Anthropic auth via claude.ai/Console — not Bedrock/Vertex/Foundry, org
channelsEnabled, allowlist /--dangerously-load-development-channels). Documented as an operational constraint, not a design flaw. - Bad, because two-way channels widen the injection surface — mitigated by the existing verification/attribution gate and the standard's sender-allowlist guidance.
Confirmation
- The channel-delivery spec defines the binding, the todo→notification mapping, delivery semantics, and the two-way (reply/permission) mapping.
- A test asserts a created/assigned todo to a channel-attached consumer emits exactly one
notifications/claude/channelwhosemeta.todo_idmatches, and thatmetakeys are identifier-safe. - A test asserts that with no attached session the todo remains
pendingand is later drained by pull — no loss (push failure is non-fatal). - A test asserts duplicate notifies do not cause double-processing (dedup + idempotent claim).
- A test asserts only verified, human-attributed todos are eligible to push, and that a payload containing
</channel>is rejected upstream.
Pros and Cons of the Options
(A) Channels as the system of record (rejected)
- Good, because simplest and lowest-latency — push the event, done.
- Bad, because delivery is lossy and unacknowledged by the standard's own definition: a closed session, an unloaded channel, or an org policy block drops the event silently. Work would vanish — the exact failure ADR-0007 exists to prevent.
- Bad, because no dedup, lease, or ownership — the primitive regresses to a fire-and-forget message.
(B) Pull-only (rejected)
- Good, because durable and dead-simple; no session-liveness handling.
- Bad, because it forfeits a real, now-available push path and pays polling latency/cost on every idle drain, even when a session is attached and ready.
(C) Notify layer over the durable queue (chosen)
- Good, because push ergonomics with zero durability loss; the queue is the floor, Channels is the accelerator.
- Bad, because it maintains both paths (a channel adapter + the queue) — accepted as the cost of "fast and safe."
Architecture Diagram
More Information
- The durable primitive this layers on, and the "MCP can't push" framing it revises: ADR-0007.
- The vended endpoint that carries the
claude/channelcapability: ADR-0008. - Human consent that permission-relay accelerates (still durable todos): ADR-0010.
- The human-side push analogue (SSE web UI): ADR-0001.
- Delivery contract, message mapping, two-way + limits: channel-delivery spec.
- Claude Code Channels reference: https://code.claude.com/docs/en/channels-reference. MCP: https://modelcontextprotocol.io/.