AI PR Gate for Claude Code + Codex — one model implements, a different model reviews the exact diff, and a signed, tamper-evident evidence trail opens as a real GitHub PR for you to merge.
Windows is not yet verified; see
docs/windows-support.md for compatibility notes.
Governance for AI-written code, at the git boundary.
claude-codex-bridge ship "<task>" turns an AI coding task into a governed PR:
create a branch, implement in a sandbox, run your test command, require a
different model to review the exact diff, produce an SSHSIG-signed evidence
envelope bound to that diff and test run, then open a real GitHub PR — you
merge. --dry-run previews the whole flow with no side effects.
What the signature means — the honest boundary. It gives you tamper-evidence, accountability, and binding to exactly what was reviewed, plus genuine cross-model review (Codex and Claude are different models) and a human merge. On a single machine it is not machine-independent cryptographic identity — true cryptographic independence is a cross-machine / team property. We'd rather say that plainly than oversell it.
We build it with the workflow it ships: recent changes are implemented by one model, reviewed by the other, and pushed through the same signed review gate.
Built for solo and founding engineers who already run both Claude Code and Codex and let AI touch real repos. Under the hood it's a small, dependency-free bridge over the Model Context Protocol: each agent reaches the other with one tool call and the Claude side keeps per-project memory — now pointed at PR governance plus live cross-model token/quota awareness so you're never blocked by one vendor's limit.
- Ship a task as a governed PR (
ship): sandboxed implement → cross-model review of the exact diff → SSHSIG-signed evidence → a real GitHub PR you merge. - See both models' live token/quota (5-hour + weekly + context) in your status line, plus an advisory pick of which model should implement vs review.
- 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.
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.
- 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 . --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.
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. - 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.
MIT — see LICENSE.
