The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Letterbox listing page.
📌 Built for internal production use. Architecture proven across months of daily AI development. Open-sourced as a reference implementation. I use it almost daily.
If you use AI coding assistants in the terminal, you normally work with one at a time — and getting two of them to collaborate means copy-pasting messages between windows yourself. Letterbox lets two assistants (say, Claude and Gemini, or Gemini and Mistral's Vibe) talk directly to each other and work a task together, hands-free.
The result: one agent can plan while another reviews, or the two can split the work between them — collaborating on their own while you watch, instead of relaying every message by hand.
A small file-based comms protocol that lets two AI agents in separate terminals talk to each other in real time.
Letterbox lets two terminal coding agents — Claude Code, Gemini CLI, Antigravity, or Mistral's Vibe — hold a real-time conversation by passing message files through a shared directory. When one agent speaks, a 📬 notification is injected into the other's terminal and wakes it to read and reply. No network, no server, no shared memory: just JSON files in a folder and the OS's atomic-rename. It's the messaging layer that was built for an internal planning loop, extracted into a standalone, versioned tool in 2026. If you've ever wanted two CLI agents to collaborate on a task without you copy-pasting between windows, this is for you. It gets the occasional update at the author's whim (the launcher tells you when a newer one is out) — but it's currently unsupported small project, not a community one.
The bridge is cross-harness: Claude on one side, Gemini on the other, talking through the same channel, has been verified live. The one wrinkle is setup — Claude wires itself automatically, while Gemini and Antigravity load letterbox from their own settings. The Setup section walks through both.
I work with two AI collaborators every day — Claude and Gemini — each living in whatever terminal harness it runs in (Claude Code, Gemini CLI, Antigravity CLI, and now Mistral's Vibe). Letterbox is how I get them talking to each other instead of through me.
That happens in two modes. Sometimes it's manual: we're brainstorming and I want to loop the other model into the conversation. Sometimes it's automated — in the planning loop, Claude drafts a plan and each plan is routed to Gemini for review as a built-in stage. Letterbox carries both the same way.
It's harness-agnostic by design — Claude Code ↔ Gemini CLI ↔ Antigravity CLI ↔ Vibe in any combination — and same-model pairs work just as well: two Claude tabs, or two Gemini tabs, talking over one channel.
Each letterbox <harness> launch runs two coordinated processes inside one terminal:
There is no daemon, no IPC, no background service. The filesystem is the coordination medium — the PTY-Parent's watcher sees a new msg-*.json appear and renders a notification; the channel directory is durable, inspectable, and cat-able. Crash recovery is trivial because nothing valuable lives in memory.
How the agent gets the letterbox tools differs per harness, and it's the one thing you configure once:
--mcp-config to claude. Nothing for you to set up.letterbox entry there once, and the launcher hands each session its channel and identity through environment variables at launch — so you never edit settings per channel.~/.vibe/config.toml. Its MCP subprocess inherits only a trimmed environment, so a one-time bridge script is needed to relay LETTERBOX_CHANNEL / LETTERBOX_SENDER from Vibe's own process env. Once that's in place, any channel works exactly like Gemini. See the Vibe setup section.LETTERBOX_NO_UPDATE_CHECK=1.)Letterbox is a two-way bridge at heart — one peer talking to one peer is what it's designed and tuned for. Three or more agents can share a channel: directed addressing (send_message(to="<label>")) and the participants list make it workable, and same-channel broadcasts reach everyone. But a shared channel is a broadcast bus — every message wakes every participant. Without orchestration (turn-taking, a designated coordinator, or rules about who speaks when), an N-way room becomes a notification storm that can chew through a model's message/usage limits surprisingly fast. If you want three or more, bring your own conductor. The substrate is honest about who's in the room; the etiquette is on you.
Available on PyPI and the MCP Registry.
Or from source:
This puts the letterbox command on your PATH. The command must resolve by name — each agent spawns letterbox mcp itself — so this is load-bearing. Confirm it:
You also need the harness you're launching (claude, gemini, antigravity, or vibe) installed, on your PATH, and logged in. Letterbox launches it for you.
Letterbox is versioned (letterbox.__version__, the single source of truth) and published to PyPI. On a human-facing launch the CLI makes one best-effort check (at most once a day, cached under ~/.cache/letterbox/) and prints a one-line notice if a newer version exists. To update:
This is the only network call letterbox ever makes — the messaging protocol stays fully local. It runs with a tight timeout and is fully fail-silent: if it can't reach GitHub it simply prints nothing and never delays your launch. It is never run for letterbox mcp (the agent's stdio server). Disable it entirely with LETTERBOX_NO_UPDATE_CHECK=1.
You only do this once per harness. Skip the harnesses you won't use.
Letterbox wires Claude automatically: at launch it writes a temporary MCP config (mode 0600) and passes --mcp-config <path> to claude. The letterbox tools appear in that session and nowhere else. There is no settings file to edit.
1. Register the MCP server in ~/.gemini/settings.json (create the file if it doesn't exist). Use the absolute path to your installed letterbox (from which letterbox above), and pass only ["mcp"] — no channel, no identity:
This entry is channel-agnostic on purpose. The launcher exports LETTERBOX_CHANNEL, LETTERBOX_SENDER, and LETTERBOX_INSTANCE_ID into Gemini's environment at launch, and the MCP server reads them — so the same single entry serves every channel and you never edit it again. (This mirrors how the Forge orchestrators pass a channel via an env var.)
2. Trust the folder you launch from. Gemini refuses to run in an untrusted directory without an interactive "do you trust this folder?" prompt — and a blocking TUI prompt would stall the automation. Pre-trust the launch directory (or a parent) in ~/.gemini/trustedFolders.json:
TRUST_FOLDER trusts exactly that directory; TRUST_PARENT trusts it and everything beneath, so one entry covers all your project folders. (Tip: don't reach for Gemini's --skip-trust flag to dodge this — it forces a workspace-system-prompt lookup that crashes even in already-trusted directories. Trust the folder instead.)
agy)Launch it as letterbox agy … (the long form letterbox antigravity … also works — agy is just an alias matching the binary name). Antigravity receives its per-launch channel and identity through the same environment variables as Gemini; what differs is how you register the MCP server. agy loads MCP servers from plugins, so you install letterbox as a tiny local plugin (a directory with two JSON files):
The mcp_config.json is channel-agnostic for the same reason Gemini's settings entry is — the launcher passes the channel and identity by environment at launch. Like Gemini, agy also gates on folder trust: it honours a trustedWorkspaces list in ~/.gemini/antigravity-cli/settings.json, so add the directory you launch from there if it isn't already.
Status: the PTY layer (notifications + message delivery, both directions) is verified live, and the plugin install above wires the tools cleanly. The full tools-in-
agyround trip is freshly working and lightly exercised — treat Antigravity as the newest of the three and report anything odd.
Launch it as letterbox vibe …. Vibe loads MCP servers from ~/.vibe/config.toml, but its MCP subprocess inherits only a trimmed environment (HOME, PATH, SHELL, TERM, USER, LOGNAME) — so LETTERBOX_CHANNEL etc. don't reach it via normal inheritance. A small one-time bridge script fixes this by reading them from Vibe's process via /proc at spawn time. After that, any channel works exactly like Gemini — no config editing per channel.
1. Install the bridge script (ships with letterbox):
2. Register it in ~/.vibe/config.toml. Replace any existing letterbox entry with:
Use your actual home path (not ~ — Vibe may not expand it). The entry is channel-agnostic on purpose: the bridge script reads the channel and identity from Vibe's process env at runtime, exactly as Gemini reads them from its environment.
3. Done. Any channel works:
Vibe also launches with --yolo (auto-approve) so injected notifications can wake it without blocking on a per-tool prompt.
Note: the bridge script uses
/proc/$PPID/environto read Vibe's environment — Linux-only, which aligns with letterbox's POSIX-only stance. macOS support would need a different mechanism (ps -p $PPID -Ewww); not currently shipped.
Status: 📬 wake injection is confirmed working (STEP 0 verified that Vibe's
ChatTextAreaoverrides Enter to submit, so the standard PTY inject path applies). Treat Vibe as the newest of the four and report anything odd.
Open two terminals and point each at the same channel with a distinct identity. With no config file, letterbox's built-in defaults supply the shared global state directory (~/.letterbox).
A genuine cross-harness bridge — Claude talking to Gemini (complete the Gemini setup first):
Or two of the same harness, if you'd rather keep it simple:
Both sessions start and sit quietly. Now nudge the agent in Terminal 1 — for example, "Send a message to your peer." From there, each 📬 notification wakes the other agent to read and reply: that handoff is the whole point. The --as <label> names make the transcript readable; underneath, message filtering uses a per-launch instance id, not the label.
A couple of honest notes:
letterbox mcp yourself. That subcommand is the stdio MCP server, spawned by the harness — it's for the agent, not for you. Run by hand in a terminal, it tells you so and exits.--dangerously-skip-permissions and the Gemini adapter with --yolo, because injected messages can't wake an agent that's blocked on a per-action approval prompt. If that's not a tradeoff you want, letterbox isn't the right fit — override the args in letterbox.toml or step away.For a full, narrated walkthrough (two Claudes debating whether a hot dog is a sandwich), see the sample project under examples/two-claudes-debating/.
Because a settings-wired harness loads letterbox on every session, an agent may have the letterbox tools available without an active bridge — for instance, a plain Gemini session you never launched through letterbox. Letterbox handles this calmly and gives the agent a way to check:
channel_info is the agent's bridge oracle. Calling it answers, server-side: is a bridge active at all? On what channel, as whom? Who is the peer (observed from its most recent message), how many unread, and when did it last speak? An agent unsure of its situation can ask before sending — "peer last spoke 90 s ago" reads very differently from "never."From any terminal, watch the raw conversation or see what channels exist:
To scaffold a starting letterbox.toml instead of relying on defaults:
check_messages returns unread peer messages and advances that agent's read marker as it goes — so successive calls page through the backlog and a drained inbox stays drained, no manual bookkeeping. check_latest_message is a non-advancing peek for the common "what did they just say?", and acknowledge is there for explicit, single-message control.check_messages with a since_id cursor); it just isn't forced on you..read/ markers track read state — they advance markers, never touch the files, and never affect the peer's view.check_messages and list-channels may show noticeable latency. Prune above that point.letterbox prune is the safe way to reclaim space. It is dry-run by default — it prints what would happen and touches nothing. --yes-i-am-sure moves matched files to a reversible cold/ subdirectory; --delete --yes-i-am-sure (double-gated) deletes for good. This is the only destructive command in letterbox.A channel is just a folder, so rm -rf ~/.letterbox/channels/demo works too — letterbox locks nothing.
The full threat model lives in docs/PROTOCOL.md. In brief:
📬 notification template substitutes variables drawn from the watcher's own configuration and observations ({channel}, {sender}, {message_id}, {timestamp}) — never from the peer's message payload. A malicious peer can write anything into its file; none of it reaches the injected notification. Message bodies are surfaced only when the agent explicitly calls check_messages. The same holds for channel_info's peer fields: they're observed from traffic and informational, never fed into a notification.execs, evals, or shells a message body or metadata field. Subprocesses are spawned with argv lists (never shell=True), and only to launch the harness configured in letterbox.toml.../etc or anything with a slash is refused.~/.letterbox/ and channel directories are created 0700 (user-only); the generated MCP config is 0600.What letterbox does NOT defend against: a compromised local user account (filesystem permissions are the only barrier), the consuming harness's own prompt-injection vulnerabilities, or trust boundaries introduced by cross-machine sync (NFS, syncthing). It is not an encryption-at-rest or network-trust layer — those are out of scope by design.
What letterbox deliberately does not do is the point, not a gap:
This anti-scope is what lets letterbox be small, inert, auditable, and durable.
--format=plain) — pipe- and screen-reader-friendly; tail emits message JSON on stdout for jq. Structured/colored output is opt-in (--format=rich).--color=auto|always|never controls color independently; color is never the only way a state is conveyed.Letterbox v1 is POSIX-only (Linux and macOS). The PTY spawn-and-inject layer is built on POSIX primitives; Windows support via the stdlib pty module is incomplete and not shipped. If you're on Windows, letterbox won't run for you in v1 — better to know now than to hit a crash.
examples/two-claudes-debating/ — the hands-on walkthrough: two Claude Code sessions debating in real time.skills/letterbox/SKILL.md — the agent-facing usage guide: how an LLM uses a live bridge (broadcast, directed messages, participants).skills/letterbox-setup/SKILL.md — the agent-facing setup guide: the one-time per-harness MCP wiring, the one-label-per-channel rule, and the post-upgrade relaunch procedure.docs/AGENT_POINTER.md — a short drop-in block to paste into a project's CLAUDE.md / GEMINI.md / AGENTS.md so an agent knows it's on a bridge.docs/PROTOCOL.md.DECISIONS.md — the architecture decision records (ADRs) behind every load-bearing choice, including the per-harness MCP wiring (ADR-054/055), dormant mode and the channel_info oracle (ADR-056), the submit-timing fix (ADR-057), the self-maintaining read marker (ADR-058), the per-channel duplicate-instance guard (ADR-061), N-party directed addressing + participants (ADR-062), and the Vibe adapter + Textual submit contract (ADR-067).LICENSE — MIT.Letterbox is a versioned, unsupported artifact — MIT-licensed, at github.com/dovahkiin-v/letterbox. It ships complete and stands as documented, and is not soliciting contributions. It is not frozen: I use it almost every day. However, I may cut a later version at their own whim, with no schedule. The launcher's once-a-day update check tells you when that happens (LETTERBOX_NO_UPDATE_CHECK=1 to silence it). See CONTRIBUTING.md for what that means in practice.