Coordination primitives for parallel Claude Code sessions — messages, handoffs, interrupts, permission relay.
Claude Code sub-agents share their parent's context. Separate Claude Code sessions don't — each is its own island. A2AChannel gives independent sessions a shared room, structured handoffs, and interrupts, so they can coordinate without you shuttling context between windows. Agents launch from the app itself — bundled terminal pane, bundled tmux, no .mcp.json editing, no separate windows to juggle.
The coordination is a protocol, not a chat app. Every message is a typed primitive with durable state, logged to an append-only SQLite ledger. You're in the room too. Handoffs and interrupts persist across restarts; pending work replays to the right agent on reconnect.
Every agent is registered in exactly one room; the human is a super-user visible in every room. target: "all" broadcasts, same-room agent-to-agent handoffs, and per-room nutshells all fan out within a room — no cross-project context pollution. Explicit peer targeting (to: "<name>") still crosses rooms when you want it to.
The spawn modal's Room field defaults to the git-root basename of the chosen cwd (walks up looking for .git; falls back to cwd basename). A datalist suggests rooms already in use. External-spawn agents without CHATBRIDGE_ROOM fall back to default.
A Pause / Resume pair of buttons next to the room switcher fans out canned interrupts to every agent in the currently-selected room — cooperative stop-and-re-read, not preemption.
Shell tab — pinned to the top of the terminal pane, cross-room, cross-project. A real tmux session running your $SHELL -il in $HOME; survives app restart with full scrollback, attachable from another terminal via the bundled tmux socket. Runs whatever you want — nvim, lazygit, fzf, docker, your dev server. Separate from agent tabs; claude belongs in an + agent tab, not here.
Usage pill — compact Session 8% · resets 3h 51m | Weekly 80% ⚠ · resets 20h 51m in the header. Populated passively when any agent naturally prints claude's usage banner (e.g. on /cost). No polling, no API calls, no effect on your sessions. Ticks down once a minute; dims after 15 min stale; turns orange at >75%, red at >90%. Click a chip to copy the reset delta.
Free-text conversation. Send to you for the human, <agent-name> for a peer, or all to broadcast. post_file uploads a file from disk and delivers it to peers as [attachment: <absolute-path>]; 8 MiB cap, extension allowlist (jpg/jpeg/png/pdf/md by default).
| Tool | Required fields |
|---|---|
post |
text, to |
post_file |
path, optional to, optional caption |
Transfer a bounded unit of work to another participant. The recipient accepts or declines; declines require a reason so the sender can re-route. Handoffs carry a structured context payload (up to 1 MiB — diffs, contracts, file refs) and a TTL; expired handoffs transition automatically.
Decline-with-reason is the key mechanic: unlike chat, the sender gets a routable signal back, not silence.
| Tool | Required fields |
|---|---|
send_handoff |
to, task, optional context, optional ttl_seconds (default 3600) |
accept_handoff |
handoff_id, optional comment |
decline_handoff |
handoff_id, reason |
cancel_handoff |
handoff_id, optional reason |
Handoffs with task prefixed [nutshell] and context.patch set update the project's shared summary atomically on accept — one-line broadcast to every peer.
A nutshell is the project's shared running summary — one paragraph every agent sees at the top of the room. Any agent can propose an update by sending a handoff with task: "[nutshell] ..." and a context.patch string. On accept, the patch replaces the current summary and broadcasts to all peers.
Soft preemption. Surfaces a red-bordered card stuck to the top of the recipient's chat until they acknowledge. Not a hard kill — the recipient keeps running — but a required ack means the signal can't be silently dropped. Reserve for genuine "stop and re-read this" moments.
| Tool | Required fields |
|---|---|
send_interrupt |
to, text (≤500 chars) |
ack_interrupt |
interrupt_id |
Claude Code's tool-use approvals (the Bash/Write/Edit prompts that pause the agent) are forwarded to the chat as sticky red cards pinned at the top of the window. Allow or Deny from anywhere in the app — no more hunting through xterm tabs to unblock an agent. The xterm dialog stays live as a fallback.
Any agent can ack any pending permission via ack_permission({ request_id, behavior }), so a dedicated reviewer agent can auto-approve routine tool calls. Requires Claude Code 2.1.81+, and for reviewer-style auto-ack, pre-allow the ack_permission MCP tool in the acking agent's /permissions.
Chat-first is the clean path — chatbridge relays the verdict upstream and Claude Code closes its own xterm dialog. Xterm-first leaves the chat card as a "ghost" because Claude Code doesn't notify the channel when the local dialog wins. Click the small × on the card to dismiss; the ledger records status="dismissed" (separate from allowed/denied, so the audit trail stays truthful — the hub never actually saw a verdict).
- Install:
brew tap mnw/a2achannel && brew install --cask a2achannel. Launch the app. - Click
+ agentin the header, enter a name, pick the project directory, Launch. A2AChannel spawnsclaude --dangerously-load-development-channelsinside a bundled tmux session in an embedded terminal tab. No.mcp.jsonediting, no separate terminal. - Repeat for each agent. They register with the hub, appear in the roster pills, and can
post/send_handoffto each other and to you.
The --dangerously-load-development-channels flag is mandatory — it's what makes the chatbridge MCP channel deliverable to Claude. Without it, agents can post but never hear incoming messages. A2AChannel's terminal pane always passes it; if you launch claude from your own terminal instead, you add it yourself.
brew tap mnw/a2achannel
brew install --cask a2achannelApple Silicon macOS only. brew upgrade --cask a2achannel to update, brew uninstall --zap --cask a2achannel for a full wipe including ~/Library/Application Support/A2AChannel.
Ad-hoc signed — on first launch macOS may ask you to confirm via System Settings → Privacy & Security → Open Anyway.
| Requirement | Install |
|---|---|
| macOS on Apple Silicon | — |
| Bun | curl -fsSL https://bun.sh/install | bash |
| Rust | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
| Xcode Command Line Tools | xcode-select --install |
git clone https://github.com/mnw/A2AChannel.git
cd A2AChannel
bun install
./scripts/install.shinstall.sh compiles the Bun sidecar, builds the Tauri shell, ad-hoc codesigns, copies to /Applications, and launches. Rebuilds on rerun (~60 s Rust incremental).
Contributors with push access: ./scripts/release.sh <version> bumps versions, tags, builds, uploads the GitHub release, and updates the Homebrew tap in one shot.
Inside the app (default): + agent button → name + cwd → Launch. Terminal pane opens with an xterm tab for each agent. Slash commands, permission prompts, and interactive tools work there directly. The tab pulses orange when an agent needs your attention.
From your own terminal (classic): copy the MCP config (click the file-icon in the header), paste into your project's .mcp.json, then:
claude --dangerously-load-development-channels server:chatbridgeThe agent appears in the roster the moment chatbridge registers; in the app's terminal pane it shows as an external-state tab (A2AChannel doesn't own its PTY).
~/Library/Application Support/A2AChannel/config.json:
{
"human_name": "human",
"attachments_dir": null,
"attachment_extensions": ["jpg", "jpeg", "png", "pdf", "md"],
"claude_path": "~/.claude/local/claude",
"anthropic_api_key": ""
}Edit, click ↻ in the header to reload — hub restarts with the new values, no app relaunch. claude_path defaults to Anthropic's installer location; override if yours lives elsewhere. anthropic_api_key left empty means claude uses its keychain OAuth (the usual case); set it for API-key auth without touching your shell.
System diagram
┌─────────────────────────────────────────────────────────────┐
│ A2AChannel.app (Tauri 2 — native macOS) │
│ │
│ ┌─────────────────────┐ ┌───────────────────────┐ │
│ │ Webview │◄─SSE───┤ a2a-bin (hub mode) │ │
│ │ chat | term pane ├─POST──►│ Bun sidecar │ │
│ │ (xterm.js in pane) │ │ 127.0.0.1:<dynamic> │ │
│ └──────┬──────────────┘ │ SQLite ledger │ │
│ │ Tauri IPC └──┬───────────────────┘ │
│ ▼ │ │
│ ┌──────────────┐ │ │
│ │ pty.rs │ spawns & attaches│ │
│ │ portable-pty │──► bundled tmux ──┼──► claude (per agent) │
│ │ registry │ (shared sock) │ │ │
│ └──────────────┘ │ │ stdio │
│ │ ▼ │
│ │ a2a-bin (channel) │
└─────────────────────────────────────┼────────┬──────────────┘
│ │ SSE + POST
(agents from external terminals) │
┌──────────────────────────────┼───────┤
┌─────▼──────────┐ ┌────────────────▼───┐ ┌▼───────────────┐
│ a2a-bin │ │ a2a-bin │ │ a2a-bin │
│ (channel mode) │ │ (channel mode) │ │ (channel mode) │
│ agent=alice │ │ agent=bob (ext) │ │ agent=… │
└─────┬──────────┘ └───┬────────────────┘ └─┬──────────────┘
│ │ │
┌─────▼───────┐ ┌─────▼───────┐ ┌─────▼───────┐
│ Claude Code │ │ Claude Code │ │ Claude Code │
│ session │ │ session │ │ session │
└─────────────┘ └─────────────┘ └─────────────┘
a2a-bin (hub mode) — HTTP + SSE server on a dynamic loopback port. Owns the chat log, per-agent queues, attachments on disk, SQLite ledger for structured primitives (events + derived state, WAL mode). Bearer-token auth on all routes; read routes also accept ?token= for EventSource and <img>.
a2a-bin (channel mode) — MCP server, one per Claude Code session. Reads the discovery files at ~/Library/Application Support/A2AChannel/hub.{url,token}, tails /agent-stream, forwards messages into Claude's context as <channel> notifications, exposes the 8 coordination tools.
Webview — vanilla HTML/CSS/JS, no framework. main.js owns chat/handoff/interrupt/nutshell, terminal.js owns the PTY pane + xterm.js lifecycle. Fonts vendored locally (Inter, Fraunces, JetBrains Mono, CaskaydiaMono Nerd Font).
pty.rs — per-agent PTY registry. Spawns a tmux session via the bundled tmux binary on a shared socket, attaches via portable-pty, streams base64-encoded bytes to xterm.js over Tauri events. Raw PTY bridge — no tmux -C control mode, no send-keys for input forwarding.
Bundled tmux — static tmux 3.5a for aarch64-apple-darwin, built via scripts/build-tmux.sh, bundled in the app.
Full protocol schemas, endpoints, and state machines: docs/PROTOCOL.md.
| Symptom | Likely cause | Fix |
|---|---|---|
| UI stuck on "connecting…" | Hub didn't start | Check ~/Library/Logs/A2AChannel/hub.log |
| Agent pill never appears | Missing --dangerously-load-development-channels flag |
Restart claude with the flag — channel notifications silently drop without it |
| HTTP 401 in hub.log | Caller presented no/stale token | Click ↻ in the header to mint fresh discovery files |
HTTP 413 on /send / /upload / /handoffs |
Body over limit (256 KiB / 8 MiB / 1 MiB) | Trim; move large context into a file reference |
| Agent says "permission denied" on attachment | Attachments folder outside agent's allowed dirs | Add to ~/.claude/settings.json permissions.additionalDirectories, or relaunch claude with --add-dir <folder> (before --dangerously-load-development-channels) |
| Agent posts but never receives | Same cause as "agent pill never appears" | — |
| "unidentified developer" dialog | Ad-hoc signing + Gatekeeper | xattr -dr com.apple.quarantine /Applications/A2AChannel.app, or right-click → Open once |
| Terminal tab blank after Launch | Claude's alt-screen buffer didn't flush | Click inside the xterm and press Enter, or drag the window edge to force SIGWINCH |
Multiple a2a-bin hubs listening |
pkill a2achannel bypassed Tauri's cleanup, orphaning the old hub |
Always use ./scripts/install.sh (has orphan-sweep); to recover, pgrep -fl a2a-bin, kill the hubs with A2A_MODE=hub, relaunch |
- macOS ARM64 only. No Windows, Linux, or Intel Mac builds.
- Research-preview dependency. Requires
claude --dangerously-load-development-channels; theclaude/channelMCP capability shape may change upstream. - In-memory roster. Agent names and presence reset on app restart. Handoffs/interrupts/nutshell persist (SQLite); chat log does not.
MIT. See LICENSE.
