Push and pull
Pull is the contract. Push is an accelerator.
Pull
Any MCP client with a token calls inbox and ack over HTTP. No development flag, no extra process. Mail waits until someone reads it. This path always works, even when push is broken or off.
Push
Push uses a second, optional MCP entry: session-bus channel, a small stdio process (the shim) that Claude Code starts for each session.
- It declares only the Claude Code channel capability (
experimental['claude/channel']). It exposes no tools. - It is an HTTP client of the bus. It never opens the database.
- It needs an explicit name (
--sessionorSESSION_BUS_NAME). Without a valid name it idles: it answersinitialize, logs one line to stderr, and does nothing else. That makes it safe to configure at user scope for everyclaude. - After Claude Code finishes the MCP handshake, it takes a per-name lock, resolves its token, and calls
register. - It then polls
inbox(after_id=<high-water mark>)everyBUS_POLL_MS(default 5000 ms). If a poll returns a full page of 50, it polls again at once. - Each new message from an allowlisted principal becomes a
notifications/claude/channelevent. Claude Code shows it as← bus-push: .... - The event carries
meta{msg_id, to, from, from_principal, thread_id}(all strings;thread_idis""when unset).
The shim never acks
The model acks after it has handled the message (ack with session=<meta.to> and ids=[<msg_id>]). If the model never does, the message stays unacked and visible to inbox.
The allowlist
--allow-from <principal,...> lists the principals whose messages are pushed. The default is the shim's own principal. Messages from other principals are not pushed. The shim still moves its cursor past them, and they stay unacked in the inbox for pull.
Cursor
The shim stores {bus_instance_id, high_water_id} in shim-<name>.json in the state directory. If the bus reports a different bus_instance_id (after restore, backup copy use, or doctor --rotate-instance), the shim resets its cursor to 0 and re-pushes unacked mail.
Errors
- Connection errors and HTTP 5xx: retry with exponential backoff, up to 60 s.
- 401 or 403: exit 2 with a message. Fix the token and restart
claude. NAME_TAKENon register: exit 4.- A second shim for the same name on the same machine: exit 3.
- If the session is released while the shim runs, the next
inboxfails withNOT_FOUNDorNOT_OWNER; the shim registers again.
Why not push only
Channels are a research preview. They need the --dangerously-load-development-channels flag and an interactive session. If push were the only path, a change in that preview would stop delivery. With pull as the contract, it only stops the shortcut.