Skip to content

jackcongmac/claude-codex-bridge

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

255 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

claude-codex-bridge

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.

License: MIT MCP Claude Code Codex Platform Python

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.

Terminal demo: Codex asks Claude for review, applies the change, then asks Claude to re-review with memory of the previous turn.

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.

What you can do with it

  • 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.md and 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=done without 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.pyclaude -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. Pass session_id to target a specific one, or new_session: true to reset.
  • Project grounding. Each call appends a system prompt telling Claude it is the project's collaborator and to read ./collaboration.md if it exists.
  • Powers: read + write, no shell. Runs with --allowedTools Read Grep Glob Edit Write TodoWrite and --permission-mode acceptEdits. No Bash. 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.py opens a local browser window, defaulting to http://127.0.0.1:8765.
  • scripts/bridge-chat.sh --interactive opens 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 via CLAUDE_CHAT_ALLOWED_TOOLS. There is no Bash access in either mode. See docs/read-only-setup.md for 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 colleague

Via 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:

  1. Detects python3, claude, and codex (override the Claude path with CLAUDE_BIN=/path/to/claude ./install.sh).
  2. Symlinks the wrapper into ~/.claude-codex-bridge/ (so git pull / bridge-update keeps it current; BRIDGE_WRAPPER_COPY=1 forces a copy).
  3. Adds [mcp_servers.claude_chat] to ~/.codex/config.toml (backing up first), pinning the detected claude path via CLAUDE_BIN.
  4. Registers Codex as a user-scope Claude MCP server (all projects), using the detected absolute codex path: 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 threadId

Both 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; the ask_claude colleague 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-reads collaboration.md when update_id changes. 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 writes thinking while 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 --interactive

It 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/project

For 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:

  1. Each agent reads collaboration_signal.json; re-reads collaboration.md only when update_id changed.
  2. On <Peer> Outbox, the waking agent runs bridge-inbox.sh pending and records ACK, CLAIM, DECLINE, or DONE with bridge-inbox.sh ack.
  3. 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 bumps collaboration_signal.json. (Don't hand-edit the board and bump the signal separately — that lock-free pattern can lose updates.)
  4. 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 . --json

REACTIVE 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 files

collaboration_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 events

How 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's update_id is newer than this watcher's high-water mark, and status=="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_turns caps the loop; any anomaly (timeout, CAS conflict, corrupt JSON, bad draft) halts to awaiting_human and 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-implementer

Presets 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_TOOLS to 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.sh refuses to push a commit with no peer SHIP/GO recorded in the review ledger (scripts/_review.py); --no-review is audited.
  • Cryptographic identity (SSHSIG). Per-actor Ed25519 keys are managed through scripts/bridge-identity.sh and scripts/_sig.py, using ssh-keygen -Y sign/verify and the public registry at .collab/keys/allowed_signers. The review ledger signs entries, the push gate verifies them and rejects unsigned or legacy GO, 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.

About

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 a human to merge — plus live cross-model token/quota awareness.

Topics

Resources

License

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors