@jackcongus/claude-codex-bridge
v1.0.0
Published
Governance for AI coding: two different models (Claude + Codex) implement and review real repo changes and ship only via a signed, tamper-evident, human-approved PR — plus live cross-model token/quota awareness so you're never blocked by one vendor's limi
Maintainers
Readme
claude-codex-bridge
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.
What you can do with it
- 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 |
Why a wrapper for the Claude side?
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.
The persistent "colleague" behavior
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.
Group chat
Two front-ends post to the same ## Chat board thread:
scripts/bridge-chat-web.pyopens a local browser window, defaulting tohttp://127.0.0.1:8765.scripts/bridge-chat.sh --interactiveopens a terminal chat panel.
Agents reply through per-agent responders (scripts/bridge-chat-respond.sh),
kept warm with session resume (codex exec resume / claude -p --resume) for
low-latency, context-carrying replies.
Shipped chat UX includes drag-drop and paste image attachments that agents can
read, @-mention with keyboard navigation, IME-safe send, honest online/offline
presence, message send status (sending / delivered / failed-retry),
markdown-rendered bubbles, a top project bar with folder name and path, and
session compaction on close with a past-sessions timeline on reopen.
Experimental chat-to-execution is off by default behind BRIDGE_CHAT_EXECUTE=1:
a judge classifies a signed human directive, then runs a sandboxed implement →
cross-review → signed-push pipeline. High-risk actions require an explicit human
greenlight; the path is safety-gated and fail-closed.
Prerequisites
- Claude Code CLI (
claude) — logged in - Codex CLI (
codex) — logged in python3(standard library only; no pip installs)
Install
⚠️ 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 with
BRIDGE_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.
Usage
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 threadIdBoth directions are global after install — every project gets them, no per-project setup.
Coordination layer (the part that makes them collaborate)
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 . --jsonREACTIVE means the agent is present and currently armed. PRESENT means the
heartbeat is fresh but the listener is not currently armed, so re-arm it or drive
it directly. STALE, DEAD, and DEPARTED mean the heartbeat is aging,
expired, or explicitly marked departed.
Autonomous mode (event-driven, no manual poke)
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".
Resource-aware routing
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.
Configuration (env vars on the Codex claude_chat server)
| 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.
Gotcha that will bite you if you reimplement this
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.
Security notes
- 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. - Review-before-merge is mechanically gated:
scripts/bridge-push.shrefuses to push a commit with no peerSHIP/GOrecorded in the review ledger (scripts/_review.py);--no-reviewis audited. - Cryptographic identity (SSHSIG). Per-actor Ed25519 keys are managed through
scripts/bridge-identity.shandscripts/_sig.py, usingssh-keygen -Y sign/verifyand the public registry at.collab/keys/allowed_signers. The review ledger signs entries, the push gate verifies them and rejects unsigned or legacyGO, and the chat execution trigger requires a valid human signature. Identity is a key, not a self-asserted name. - The push gate also checks a rebased commit's content by patch-id, so a rebase that changes what ships is rejected for re-review.
License
MIT — see LICENSE.
