Let Claude Code and Codex call each other as tools — and make the Claude side a persistent, project-aware colleague, not a fresh stateless instance every call.
Windows is not yet verified; see
docs/windows-support.md for compatibility notes.
Claude Code ↔ Codex, wired together through MCP and a durable shared board.
This project was built with the same Claude + Codex collaboration workflow it enables: one agent asks the other for review, execution, and second opinions, while both coordinate through project-local shared files.
It is a collaboration framework for asymmetric agents: MCP is the transport,
collaboration.md is the shared memory, collaboration_signal.json is the cheap
change detector, and resource profiles keep quota, context, billing, and write
authority visible.
A small, dependency-free bridge over the Model Context
Protocol. Each agent reaches the other with one
tool call; the Claude side keeps per-project memory and reads your shared
collaboration.md so the two agents actually work together.
- Ask Claude to review Codex's current implementation without leaving Codex.
- Ask Codex to run or inspect a task from Claude Code.
- Keep a persistent Claude colleague per project directory.
- Coordinate two agents through
collaboration.mdand a low-token signal file. - Route Claude Max / Codex Pro style workflows by scarce reasoning vs. bounded execution.
- Run an event-driven collaboration loop with explicit caps and safety checks.
- Promote real reviewer/executor loops that converge to
status=donewithout a human manually poking every turn.
See examples/review-loop.md for a copy-pasteable
workflow that demonstrates the core review → execute → re-review loop.
See examples/autonomous-review-loop.md
for a real bounded autonomous review loop driven by the harness.
See examples/cookbook.md for review, test, docs, and
shared-board handoff prompts.
See docs/resource-aware-routing.md for the
full subscription, billing, context, and permission routing matrix, and
docs/role-presets.md for preset files you can apply
to collaboration_state.json. Future multi-CLI support is intentionally parked
behind docs/adapter-rfc.md; today this bridge supports
Claude Code + Codex.
┌──────────────┐ mcp__codex__codex ┌──────────────┐
│ Claude Code │ ───────────────────────────► │ Codex │
│ (you) │ codex mcp-server │ │
│ │ ◄─────────────────────────── │ │
└──────────────┘ mcp__claude_chat__ask_claude└──────────────┘
│
▼
claude_chat_mcp.py (claude -p)
│
▼
a Claude colleague that REMEMBERS (per-project session)
and reads ./collaboration.md before answering
| Direction | Tool the caller uses | What runs under the hood |
|---|---|---|
| Claude → Codex | mcp__codex__codex / mcp__codex__codex-reply |
codex mcp-server (Codex's built-in MCP mode) |
| Codex → Claude | mcp__claude_chat__ask_claude |
claude_chat_mcp.py → claude -p |
Codex ships an MCP server mode (codex mcp-server) that exposes a single "run a
Codex session" tool — perfect for Claude→Codex. Claude Code's built-in
claude mcp serve, however, exposes Claude's individual tools (Read, Bash,
Edit, …), not a "chat with Claude" endpoint, and its Agent tool has no
sub-agents registered in headless serve mode (Available agents: is empty). So
to let Codex talk to a reasoning Claude, this project wraps claude -p
(headless print mode) in a tiny MCP server. That wrapper is the mirror image of
codex mcp-server.
ask_claude(prompt, session_id?, new_session?):
- Memory, auto-pinned per directory. The first call in a working directory
creates a Claude session with a fixed id and stores it under
~/.claude-codex-bridge/sessions/. Every later call from that directory auto---resumes it, so the same Claude accumulates context — Codex never has to manage a session id. Passsession_idto target a specific one, ornew_session: trueto reset. - Project grounding. Each call appends a system prompt telling Claude it is
the project's collaborator and to read
./collaboration.mdif it exists. - Powers: read + write, no shell. Runs with
--allowedTools Read Grep Glob Edit Write TodoWriteand--permission-mode acceptEdits. NoBash. No MCP servers are loaded inside the spawned Claude (--strict-mcp-config --mcp-config '{"mcpServers":{}}'), which keeps startup fast and prevents it from recursing back into Codex.
What it is not: a literal always-on process that sees messages typed into your interactive Claude window in real time. CLI agent sessions can't be injected into from outside. Persistent session + shared
collaboration.md+ on-demand reach gives you ~90% of "online colleague" without that.
- Claude Code CLI (
claude) — logged in - Codex CLI (
codex) — logged in python3(standard library only; no pip installs)
⚠️ Security, up front. By default the Claude colleague can read and edit/write files in whatever directory Codex calls it from, using your Claude credentials — i.e. Codex can drive file changes on your machine through Claude. Install read-only withBRIDGE_READONLY=1 ./install.sh, or scope it later viaCLAUDE_CHAT_ALLOWED_TOOLS. There is noBashaccess in either mode. Seedocs/read-only-setup.mdfor the safest evaluation setup and a redacted config check.
git clone https://github.com/jackcongmac/claude-codex-bridge.git
cd claude-codex-bridge
./install.sh # read + write colleague (default)
# BRIDGE_READONLY=1 ./install.sh # read-only colleagueVia npm (packaged; publish pending). The package is built; once it's published to npm this is the frictionless path — one artifact installs/updates BOTH halves:
npm install -g @jackcongus/claude-codex-bridge # (after the package is published)
claude-codex-bridge install # wires up both MCP directions + the skill
npm update -g @jackcongus/claude-codex-bridge # update (npm owns updates here)(The package is scoped — @jackcongus/claude-codex-bridge — because the unscoped
name is taken by an unrelated project. The CLI command stays claude-codex-bridge.)
(claude-codex-bridge update / scripts/bridge-update.sh do a git pull — that's the
updater for the git-clone install above, not the npm one.) Until the package is
published, use the git clone.
The installer is idempotent. It:
- Detects
python3,claude, andcodex(override the Claude path withCLAUDE_BIN=/path/to/claude ./install.sh). - Symlinks the wrapper into
~/.claude-codex-bridge/(sogit pull/bridge-updatekeeps it current;BRIDGE_WRAPPER_COPY=1forces a copy). - Adds
[mcp_servers.claude_chat]to~/.codex/config.toml(backing up first), pinning the detectedclaudepath viaCLAUDE_BIN. - Registers Codex as a user-scope Claude MCP server (all projects), using the
detected absolute
codexpath:claude mcp add codex -s user -- "$CODEX_BIN" mcp-server.
Then restart Codex so it loads the new server.
In Codex:
call mcp__claude_chat__ask_claude with prompt:
"Read collaboration.md and give me your QA verdict on the current draft."
In Claude Code:
call mcp__codex__codex with prompt:
"Run the test suite and report failures."
# continue with mcp__codex__codex-reply using the returned threadId
Both directions are global after install — every project gets them, no per-project setup.
The two MCP servers are the transport — the "phone line." They let each agent call the other, but a phone line alone isn't teamwork. The coordination layer is a simple, durable convention that turns two agents into colleagues:
collaboration.md— a shared board in your project root: roles, operating rules, each agent's outbox, file locks, open questions, a decision log. Both agents read it before acting and write findings back to it. It's the shared memory; theask_claudecolleague is already told to read it automatically.collaboration_signal.json— a tiny low-token signal file. Instead of re-reading the whole board on every poll, an agent reads this first and only re-readscollaboration.mdwhenupdate_idchanges. Cheap polling.chat_delivery.json— responder delivery state for the group chat. The board remains the message truth; this file records which chat message ids each agent has handled so a restarted responder can replay missed @ messages with best-effort handled-id dedupe. Delivery is at-least-once: a crash after posting a reply but before recording handled state can re-answer that one message on restart.chat_typing.json— transient group-chat UX state. A responder writesthinkingwhile Claude/Codex is generating a reply, and the web chat hides stale entries automatically if a responder dies before clearing them.
For the simplest "virtual chat board" in the current terminal, run:
scripts/bridge-chat.sh --self Jack --interactiveIt posts to the same ## Chat board thread, starts the Claude/Codex responders by
default, sends on Enter, and exits on Esc without opening a browser. The older
--watch mode remains read-only live tailing. The terminal panel also shows whether
the Claude/Codex responders currently look online or offline.
When the web group chat is open, it starts one responder for Claude and one for Codex
and lightly supervises them: if a responder process exits while the room is still open,
the server starts that responder again. The room's status area shows typing state plus
responder online/offline health. By default it tries 127.0.0.1:8765; if that port is
busy, it falls back to a free local port and prints the URL to use. This is scoped to
the chat server lifetime; it is not a full always-on watcher service.
For an always-on group-chat responder loop without opening a UI, run:
scripts/bridge-chat-supervise.py --project .It starts one Claude responder and one Codex responder, restarts either one if it
exits, writes a heartbeat/state file at .collab/chat_supervisor.json, backs off
instead of hot-looping on repeated crashes, and posts a ## Liveness alert when a
responder stays down or hits the restart limit. It stops both responder process
groups on Ctrl-C/SIGTERM. This is just the responder supervisor; use
bridge-chat.sh --interactive or bridge-chat-web.py as the human-facing chat
surface. The supervisor does not supervise itself: if it dies, the stale heartbeat
is how humans/agents notice and restart it.
Drop both into a project (idempotent, never overwrites existing files):
scripts/init-collaboration.sh # into the current directory
scripts/init-collaboration.sh /path/to/projectFor a fresh or restarted agent window, run the activation autostart first:
scripts/board-wait.sh --self Codex --project . &
scripts/bridge-autostart.sh --self Codex --peer Claude --project .board-wait must be started by the agent/harness as the tracked background task;
its exit is what wakes the agent. bridge-autostart then performs the proactive handshake:
it joins the board, starts liveness, runs bridge-handshake, and reports
GO/NO-GO. A NO-GO leaves a board invite for the peer and prints the exact fix, but it
is non-blocking for work that does not require a peer handoff.
For a manual fallback, bring only the liveness side online with:
scripts/bridge-live.sh --self Codex --project .bridge-live registers you on the board, starts one presence-keepalive
process if needed, prints the current liveness report, and gives you the exact
board-wait command to run in the background. The default command exits on peer
updates, not quiet timeouts; --stay-armed is an optional liveness/pong helper and
is not the interactive pane's wake task.
When board-wait wakes on the peer's Outbox, treat that Outbox as your Inbox:
scripts/bridge-inbox.sh pending --self Codex --project .
scripts/bridge-inbox.sh ack --self Codex --project . --status CLAIM --note "what I will do"The receipt is written to Inbox Acks plus .collab/inbox_ack.json, not back to
Outbox, so a handoff becomes machine-checkable without creating a new task loop.
The loop:
- Each agent reads
collaboration_signal.json; re-readscollaboration.mdonly whenupdate_idchanged. - On
<Peer> Outbox, the waking agent runsbridge-inbox.sh pendingand recordsACK,CLAIM,DECLINE, orDONEwithbridge-inbox.sh ack. - Each posts status / findings to its own outbox via
scripts/bridge-post.sh --self <You> --message "…"— one locked step that appends to the board AND bumpscollaboration_signal.json. (Don't hand-edit the board and bump the signal separately — that lock-free pattern can lose updates.) - Use the MCP bridge to poke the other agent to take a turn.
Transport (global, installed once) vs. coordination (per-project files you drop in). The bridge gives you both halves; the board is what makes a review → execute → re-review loop actually work.
Check whether the joined agents are alive without mistaking the normal
board-wait re-arm gap for a dead peer:
scripts/bridge-liveness.sh report --self Codex --project .
scripts/bridge-liveness.sh report --self Codex --project . --jsonLIVE means the agent is present and currently armed. PRESENT means the
heartbeat is fresh but the listener is not currently armed. STALE,
DEAD, and DEPARTED mean the heartbeat is aging, expired, or explicitly marked
departed.
Manual mode needs a human to invoke each turn. Autonomous mode runs the loop itself: a lightweight watcher fires a headless agent turn whenever the other agent commits. Idle cost is ~0 (a file watcher / mtime poll — no tokens until there's real work).
# one per side: two shells on the SAME machine (or a shared filesystem) — the board
# lives in local .collab/, not synced across machines. Read-only by default.
scripts/watch-collaboration.sh --as claude --project .
scripts/watch-collaboration.sh --as codex --project . # add --allow-write to let it edit filescollaboration_state.json is the authoritative control state (ships paused).
To start the loop, set status:"active" and next_actor to whichever agent
moves first, then bump collaboration_signal.json. Watch it live:
scripts/bridge-status.py --project . --watch # state, routing, caps, recent events
tail -f collaboration_auto.log # raw JSONL eventsHow a turn is made safe (all in _auto_turn.py, not left to the model):
- Whose turn: a turn runs only if
next_actor == self, the signal'supdate_idis newer than this watcher's high-water mark, andstatus=="active". - Draft → commit-under-lock: the model only returns a JSON draft; the harness
takes a global lock, re-checks
update_id(CAS), then writes the board, state, and signal (signal last = commit marker). Two agents can't write concurrently. - Bounded & safe:
max_turnscaps the loop; any anomaly (timeout, CAS conflict, corrupt JSON, bad draft) halts toawaiting_humanand notifies — it never silently drops or loops away your budget.
Caps & safety knobs (in collaboration_state.json, or --max-turns/--max-cost
on the watcher):
| Knob | Effect |
|---|---|
status |
active runs; paused/done/awaiting_human stop the loop |
max_turns |
hard cap on total turns (governs both sides) |
max_cost_usd |
USD cap — Claude side only; codex exec cost isn't parsed, so the Codex side is governed by max_turns |
--allow-write |
lets that side edit project files (default: read-only, no Bash) |
Kill switch: Ctrl-C the watcher, or set status:"paused"/"done".
Autonomous mode is not meant to alternate turns blindly. The default templates
include resource_profiles for a max-claude-pro-codex style split:
- Claude Max handles high-leverage reasoning, architecture, strict review, test strategy, and final QA.
- Codex Pro handles bounded implementation, search, small fixes, test iteration, and mechanical docs updates.
scripts/bridge-status.py --project . displays the current roles, resource
profiles, turn caps, cost cap, last signal, recent events, and halt reason. See
docs/resource-aware-routing.md for the
routing rules.
Apply an opinionated role preset when you want the state file to reflect that split explicitly:
scripts/apply-role-preset.py --project . --preset max-claude-pro-codex
scripts/apply-role-preset.py --project . --preset reviewer-implementerPresets reuse the existing roles and resource_profiles fields; they do not
grant extra write permissions. Apply them while the loop is paused; the command
refuses status:"active" or an existing collaboration.lock unless --force
is passed. See docs/role-presets.md.
| Variable | Default | Purpose |
|---|---|---|
CLAUDE_BIN |
auto-detected | path to the claude CLI |
CLAUDE_CHAT_SESSION_DIR |
~/.claude-codex-bridge/sessions |
pinned-session store |
CLAUDE_CHAT_TIMEOUT |
900 |
per-call timeout (seconds) |
CLAUDE_CHAT_ALLOWED_TOOLS |
Read Grep Glob Edit Write TodoWrite |
tool allowlist for the colleague |
To make the colleague read-only, set
CLAUDE_CHAT_ALLOWED_TOOLS="Read Grep Glob". To give it shell access, add
Bash (understand the risk: Codex could then drive arbitrary commands on your
machine through Claude).
See docs/read-only-setup.md for a safe setup guide.
An MCP stdio server must not read its input with for line in sys.stdin.
On a pipe, Python block-buffers that iterator (it waits to fill ~8 KB before
yielding a line), so a lone initialize message never reaches your handler and
the MCP client hangs forever. It looks like it works when you feed several
messages at once in a test, then mysteriously hangs against a real client that
sends one message and waits. Use while True: line = sys.stdin.readline()
instead — it returns as soon as a newline arrives.
- The spawned Claude runs with your Claude Code credentials and can read (and
by default edit) files in the working directory. Scope
CLAUDE_CHAT_ALLOWED_TOOLSto your comfort level. - Never commit your real
~/.codex/config.toml— it may contain API keys for other MCP servers. This repo only ships a config template via the installer.
MIT — see LICENSE.
