Skip to main content
Glama
ar36planet

claude-codex-bridge

by ar36planet

claude-codex-bridge

CI

Позволяет Claude Code общаться с тем самым Codex TUI, который у вас перед глазами — и вы всё видите. Наоборот, Codex может отправлять сообщения в сессию, в которой работает Claude Code.

Это не захват экрана, не опрос файлов, не headless subagent. Оба подключены к одному и тому же потоку codex app-server: сообщение, отправленное Claude Code, мгновенно появляется в TUI, который вы видите.

English: README.en.md

Инструкция по установке: SETUP.md (中文) · SETUP.en.md (English)

В этом README описаны обоснование дизайна и записи проверок — почему выбран такой подход, какие факты уже измерены, а какие ещё нет. Если хотите сразу запустить, быстрее будет через SETUP.

Среда проверки: codex-cli 0.147.0, протестировано на Windows 11 и macOS 26, оба работают на Node 24 LTS (Krypton). Код не привязан к платформе (пути везде через node:path, resolveCodex() — единственное исключение для Windows). Различия и результаты для обеих платформ см. в разделе «Проверено / не проверено».

Требования к Node: >=22 (engines в package.json), рекомендуется v24 LTS. При каждом push в CI запускается полная матрица ubuntu / macOS / Windows × Node 22, 24.

codex часто является глобальным пакетом под какой-то версией nvm, и эта версия может быть ниже 22 — тогда node по умолчанию тоже устаревает. Самый чистый способ — держать node и codex в одном LTS:

nvm install 24 && nvm alias default 24
nvm reinstall-packages 20        # 把 codex 等全域套件搬過去(20 換成你原本的版本)

(Бин codex — это shim с #!/usr/bin/env node, он запускается с node из PATH, не привязан к версии при установке.)

Архитектура

        ┌──────────────────────────────┐
        │  codex app-server            │   ← 真正持有 thread 的地方
        │  --listen ws://127.0.0.1:8787│
        └───────┬──────────────┬───────┘
                │              │
   codex --remote ws://…       │  JSON-RPC over ws
                │              │
        ┌───────┴──────┐  ┌────┴─────────────┐
        │  Codex TUI   │  │  Claude Code     │
        │ (你在看)    │  │ (scripts/talk) │
        └──────┬───────┘  └────┬─────────────┘
               │               ▲
               └───────────────┘
        .bridge-inbox/<name>.jsonl → Stop hook
             (反方向:Codex → Claude Code)

Ключевой момент в прямом направлении — семантика thread/resume:

If thread_id identifies a running thread, app-server rejoins that thread.

Таким образом, второй клиент не открывает новый диалог и не воспроизводит сохранённое — он присоединяется к тому же самому выполняющемуся потоку. Только после присоединения он сможет получать поток уведомлений этого потока; просто подключиться к endpoint недостаточно.

Related MCP server: Claude-Gemini MCP Integration Server

Использование

Три окна.

1. Общий сервер (держите открытым, не закрывайте)

node scripts/serve.mjs --cwd C:\path\to\你的專案

--cwd — это рабочая директория, где фактически работает Codex. Если поток не указывает свою cwd, он использует cwd app-server, поэтому без этого параметра он остановится в директории, откуда вы запустили скрипт (то есть в папке самого bridge). Можно также использовать переменную окружения CODEX_BRIDGE_CWD. Порт задаётся через --port или CODEX_BRIDGE_PORT (по умолчанию 8787). Endpoint и workspace записываются в .bridge.json, talk.mjs читает их автоматически.

2. Codex TUI, который вы хотите видеть

codex --remote ws://127.0.0.1:8787 -C C:\path\to\你的專案

-C фиксирует workspace этого окна; если не указан, используется --cwd из пункта выше.

Сначала скажите что-нибудь в TUI и дождитесь ответа. Поток становится возобновляемым только после завершения первого раунда диалога; до этого thread/resume вернёт no rollout found for thread id.

Обратите внимание на ловушку: talk.mjs list видит этот поток ещё до этого — как только TUI подключается, поток создаётся и появляется в thread/loaded/list. Так что «видно в list» не означает «можно отправлять сообщения». Если пропустить этот шаг, say отправится, Codex ответит в TUI, но bridge не получит поток ответа и просто дождётся таймаута (на macOS этот симптом проверен).

3. Со стороны Claude Code

node scripts/talk.mjs list               # 列出活著的 thread(含各自的 cwd)
node scripts/talk.mjs say "跑一下測試"     # 送話進去,你會在 TUI 看到
node scripts/talk.mjs read               # 讀完整 thread(結構化 JSON)

Если поток только один, say / read выберут его автоматически; если несколько, нужно указать --thread <id> — не угадываем, с какой сессией вы говорите. list выводит cwd каждого потока, что помогает различать их при нескольких открытых.

say также принимает --cwd <dir> (меняет рабочую директорию только для этого раунда) и --approvals (см. ниже).

MCP-интерфейс

CLI по-прежнему можно использовать напрямую; MCP-интерфейс предоставляет структурированные инструменты с той же основной функциональностью. Оба направления используют один и тот же код, но каждый инициатор запускает свой STDIO-процесс:

  • --role claude: Claude Code активно отправляет сообщения в поток Codex.

  • --role codex: Codex активно помещает сообщения в почтовый ящик Claude.

Шаги установки

0. Проверьте предварительные условия

  • Node >=22 (см. примечание о версиях в начале).

  • В этом репозитории уже выполнен npm install.

  • MCP — это только интерфейс, не транспортный уровень. Для работы ему всё равно нужен запущенный serve.mjs и подключённый TUI, чтобы было с чем общаться — см. «Использование» выше.

cd <這個 repo>
npm install

1. Решите, какую сторону устанавливать

Что вы хотите

Что устанавливать

Только Claude Code может отправлять сообщения в Codex

Только --role claude (со стороны Claude Code)

Только Codex может оставлять сообщения для Claude Code

Только --role codex (со стороны Codex)

Двусторонняя связь

Установите обе

Если нужна только односторонняя связь, не устанавливайте обе стороны. Пассивная приёмная сторона не использует MCP — она работает через app-server/TUI и Stop hook от Claude соответственно.

2. Установка

Со стороны Claude Code (--role claude):

# macOS / Linux
claude mcp add --scope project claude-codex-bridge -- \
  node /path/to/claude-codex-bridge/scripts/mcp.mjs --role claude
# Windows
claude mcp add --scope project claude-codex-bridge -- `
  node "C:/path/to/claude-codex-bridge/scripts/mcp.mjs" --role claude

Со стороны Codex (--role codex):

# macOS / Linux
codex mcp add claude-codex-bridge -- \
  node /path/to/claude-codex-bridge/scripts/mcp.mjs --role codex
# Windows
codex mcp add claude-codex-bridge -- `
  node "C:/path/to/claude-codex-bridge/scripts/mcp.mjs" --role codex

--scope project запишет в .mcp.json этого проекта; для общего использования между проектами замените на --scope user.

Путь должен быть абсолютным, но «в какой директории запускать» не влияет на результат — все файлы состояния (.bridge.json, .bridge-inbox/, .bridge-output/) разрешаются относительно расположения модуля, а не cwd. Так что одной установки достаточно, не нужно устанавливать отдельно для каждого проекта.

3. Перезапустите

claude mcp add / codex mcp add только изменяют файлы конфигурации, уже запущенная сессия не загрузит новый MCP-сервер. После установки закройте и снова откройте эту сессию, чтобы инструменты стали доступны.

4. Проверьте установку

В перезапущенной сессии вызовите bridge_status. Если ok: true и role правильный, всё в порядке. Затем codex_threads_list должен показать ваш поток TUI (включая его cwd).

Если не хотите открывать сессию, можно проверить тот же путь из командной строки:

npm run test:e2e:mcp-send        # 需要 serve + 已跑完第一輪的 TUI

Инструменты

Роль

Инструменты

Общие

bridge_status, bridge_output_read

Claude

codex_threads_list, codex_thread_read, codex_message_send

Codex

claude_mailboxes_list, claude_mailbox_peek, claude_message_send

codex_message_send ожидает завершения всего раунда, поэтому для MCP-конфигурации Codex рекомендуется увеличить таймаут и требовать подтверждения для write-инструментов:

[mcp_servers.claude-codex-bridge]
tool_timeout_sec = 360
default_tools_approval_mode = "writes"

В режиме MCP допускается только CODEX_BRIDGE_APPROVALS=tui (по умолчанию) или decline, автоматическое accept не принимается. Короткие ответы отправляются inline; если превышают 64 KiB, записываются в .bridge-output/, возвращается opaque artifact ID с TTL, который затем читается постранично через bridge_output_read. Один capture по умолчанию не более 10 MiB, чтобы не помещать неограниченный ответ в один tool result или кучу Node.

Локальная проверка:

npm test                         # 語法、unit、in-memory MCP、真實 STDIO smoke;不呼叫模型
npm run test:integration:mcp-app-server  # 真實 app-server 連線,不建立模型 turn
npm run test:spikes              # 真實 app-server regression,可能使用模型
npm run test:e2e:mcp-send        # 完整 MCP → 真實 TUI;需要 serve + 已跑完第一輪的 TUI

test:e2e:mcp-send отличается от других spike: он не запускает отдельный app-server, а подключается к вашему текущему TUI через .bridge.json и создаёт реальный раунд в потоке, который вы видите. Поэтому он не входит в test:spikes и запускается отдельно.

Обратное направление: Codex → Claude Code

У Claude Code нет аналога app-server, нет сокета, в который можно что-то отправить. У него есть Stop hook: он запускается перед тем, как Claude завершает работу; если hook возвращает {"decision":"block","reason":...}, он заставляет Claude не останавливаться и продолжает работу с reason в качестве нового ввода.

Поэтому мы используем почтовый ящик. Почтовый ящик именованный — потому что может быть несколько сессий Claude Code, слушающих одновременно; если использовать общий файл, кто первый завершится, тот и проглотит чужие письма:

# Codex 那側(或任何地方)留話
node scripts/inbox.mjs push --to bridge "順便幫我看一下 auth 那段"

# 現在有誰在聽(含各自的工作目錄)
node scripts/inbox.mjs list

# 看某個信箱(不消耗)
node scripts/inbox.mjs peek --as bridge

--to — «кому предназначено это письмо», --as — «кто я, читающий», оба по умолчанию берут $CODEX_BRIDGE_MAILBOX, затем default.

В этом репозитории .claude/settings.json уже содержит Stop hook (имя почтового ящика bridge). Когда Claude Code завершает работу в этом проекте, он автоматически очищает почтовый ящик и продолжает работу. Этот файл находится под контролем версий, поэтому если вы клонируете репозиторий и откроете его в Claude Code, этот hook будет активен — когда почтовый ящик пуст, он полностью молчалив; если не нужен, просто удалите .claude/settings.json. Сообщение доставляется только один раз: drain() сначала переименовывает, затем читает, так что одновременная запись не приведёт к чтению наполовину.

Подробности и компромиссы см. в docs/reverse-channel.md.

Как использовать этот мост с другими сессиями Claude Code

Сервер запускается один раз, остальные сессии используют его совместно. Состояние скрипта (.bridge.json, почтовые ящики) разрешается относительно расположения самого модуля, а не cwd, поэтому вызов с абсолютным путём из любой директории корректен.

Прямое направление (эта сессия → Codex): настройка не требуется, вызывайте напрямую.

$bridge = "C:\path\to\claude-codex-bridge"
node "$bridge\scripts\talk.mjs" list
node "$bridge\scripts\talk.mjs" say --thread <threadId> "..."

При нескольких открытых TUI обязательно используйте --thread — list покажет cwd каждого потока для идентификации. (Или установите CODEX_BRIDGE_URL, чтобы не зависеть от .bridge.json.)

Обратное направление (Codex → эта сессия): нужно добавить Stop hook в .claude/settings.json этого проекта и дать ему своё имя почтового ящика:

{ "hooks": { "Stop": [ { "matcher": "*", "hooks": [
  { "type": "command",
    "command": "node \"C:/path/to/claude-codex-bridge/scripts/inbox.mjs\" hook --as web" }
] } ] } }

Обратите внимание: нельзя использовать $CLAUDE_PROJECT_DIR — он укажет на сам проект, а не на bridge. Путь должен быть жёстко прописан до bridge. Имя почтового ящика (в примере web) выбираете сами, по одному на сессию.

После этого со стороны Codex можно отправлять письма по имени:

node scripts/inbox.mjs push --to web "先把 CORS 那條修掉"
node scripts/inbox.mjs list       # 確認名字沒打錯、對方還活著

Данные list берутся из саморегистрации Stop hook каждой сессии при каждом выполнении, поэтому сессия должна завершиться хотя бы один раз, чтобы появиться в списке.

Подтверждения (когда Codex хочет что-то изменить)

Если раунд, отправленный Claude Code, требует выполнения команд или изменения файлов, Codex отправляет запрос на подтверждение. app-server рассылает такой запрос всем подключённым клиентам; кто первый ответит, тот и считается (остальные получают serverRequest/resolved). Поэтому стратегия по умолчанию — tui: bridge молчит, решение принимает подсказка в окне, которое вы видите.

Если никто не ответит, весь раунд зависнет, поэтому есть защита: если по истечении CODEX_BRIDGE_APPROVAL_TIMEOUT_MS (по умолчанию 300 секунд) никто не ответил, bridge сам отклоняет (fail-closed), и раунд продолжается.

node scripts/talk.mjs say --approvals decline "..."   # 沒開 TUI 時用
node scripts/talk.mjs say --approvals accept  "..."   # 只用在你已經信任的環境

Можно также установить значение по умолчанию через CODEX_BRIDGE_APPROVALS.

Маршрутизация рассылки подтверждена на macOS (spike-approvals.mjs 11/11): один клиент нажимает подтверждение, другой молчаливый клиент получает тот же запрос, затем получает serverRequest/resolved, раунд завершается нормально. Таким образом, «молчание bridge = решение за человеком» работает.

Две настройки, которые незаметно отключают «передачу решения человеку в TUI»

Прежде чем запрос на подтверждение достигнет любого клиента, он сначала проходит через настройки codex самого пользователя. Если активно любое из следующих двух, ваш TUI вообще не получит запрос, и молчание bridge не будет означать, что решение принимает человек:

Настройка

Местоположение

Эффект

Hook PermissionRequest

~/.codex/hooks.json

Hook перехватывает запрос на подтверждение. На macOS проверено: при активном hook оба клиента не получают ни одного запроса на подтверждение, файлы всё равно записываются

approvals_reviewer = "auto_review"

~/.codex/config.toml

Передаётся subagent, который автоматически решает на основе риска, не спрашивая человека

Обе настройки являются разумными личными настройками; этот проект не будет их изменять; просто нужно знать: когда они активны, «человек» в --approvals tui — это на самом деле они. Чтобы проверить, какая ситуация на вашей машине, запустите spike-approvals.mjs — этот spike запускает свой собственный app-server с --disable hooks -c approvals_reviewer=user, отключая обе настройки, и измеряет сам протокол.

Форматы ответов на различные запросы подтверждения не единообразны — только два item/*/requestApproval принимают {decision:"decline"}; item/permissions/requestApproval требует (пустой) профиль разрешений, старые execCommandApproval / applyPatchApproval требуют {decision:{denied:{rejection}}}. Неправильная форма — это ошибка схемы, а не вежливый отказ. Таблица соответствия в DEFAULT_SERVER_REQUEST_RESPONSES в src/appServerWsClient.mjs.

Почему не другие подходы

Подход

Проблема

wezterm cli send-text / get-text

Захватывает отрендеренный TUI: символы рамок, спиннеры, обрывы строк; определить, «закончил ли ответ», можно только опросом изменений экрана

Общий файловый почтовый ящик (прямое направление)

Работоспособно, но не видно состояния в реальном времени, и требуется ручное вмешательство для триггера

Subagent /codex:rescue

Каждый раз холодный старт, отдельная сессия, не подключается к тому TUI, который вы видите

Данное решение

Структурированные события; turn/steer позволяет даже вмешиваться в выполняющийся раунд

Обратное направление по-прежнему использует файловый почтовый ящик — но потому что у Claude Code нет сокета для подключения, а Stop hook делает «триггер» автоматическим, без ручного вмешательства.

Безопасность

  • Listener привязан к loopback. --ws-auth действует только на non-loopback, поэтому на локальной машине токен не требуется.

  • Подтверждения по умолчанию передаются человеку (tui), при таймауте fail-closed. Запросы от сервера к клиенту, не связанные с подтверждением (вызов инструментов, MCP elicitation), всегда fail-closed — у bridge нет UI, чтобы спрашивать человека.

Проверено / не проверено

Три spike, каждый запускает свой собственный app-server (ephemeral port), не затрагивая ваш текущий поток:

node scripts/spike-multiclient.mjs   # 9/9   兩個 client 共用一條 thread
node scripts/spike-multithread.mjs   # 7/7   兩條 thread 同時跑,回覆不串味
node scripts/spike-approvals.mjs     # 11/11 於 macOS;Windows 上 3 項 SKIP,見下

На macOS (26.5.1, Node v24.19.0 LTS, codex-cli 0.147.0) результаты: все три spike пройдены, npm test 21/21, npm run test:integration:mcp-app-server PASS. scripts/serve.mjs --cwd, scripts/talk.mjs list, scripts/inbox.mjs (push / list / peek / hook, включая китайский) также протестированы вручную на macOS; inbox hook работает даже на Node 20, так что Stop hook не требует определённой версии node.

Кроме того, проверено на реальном TUI (codex --remote): сообщение, отправленное Claude Code, отображается в TUI как сообщение пользователя, Codex нормально отвечает, поток ответа возвращается в Claude Code.

Выяснено (codex-cli 0.147.0):

  • Уведомления всегда содержат threadId, потоковые (например, item/agentMessage/delta) дополнительно содержат turnId, и turn.id, возвращаемый turn/start, полностью совпадает с тем, что в потоке. Ранее считалось, что «уведомления не содержат threadId», на самом деле это был симптом того, что клиент не присоединился к потоку.

  • Поток должен быть сначала присоединён через thread/resume, чтобы получать уведомления; а поток становится возобновляемым только после завершения первого раунда диалога.

  • historyMode: "paginated" (поток, созданный TUI) → thread/read с includeTurns завершается ошибкой (list_turns is not supported yet). Получение истории постфактум в настоящее время не работает; ответы получаются через поток в реальном времени.

  • Подтверждения рассылаются всем клиентам, засчитывается ответ первого (подтверждено на macOS). Молчаливый клиент также получает запрос, затем получает serverRequest/resolved, раунд не зависает. На машинах, где не запускается OS sandbox helper (на некоторых управляемых корпоративных Windows может появляться ShellExecuteExW failed to launch setup helper: 1223), запись файла завершается ошибкой до того, как будет задан вопрос человеку, и запрос на подтверждение вообще не отправляется, поэтому эти три пункта будут помечены как SKIP — ограничение среды, не проблема протокола.

  • Пользовательский hook PermissionRequest полностью перехватывает запросы на подтверждение, клиент не получает ни одного (проверено на macOS). Подробности см. в разделе «Подтверждения» выше.

Не проверено:

  • turn/steer (вмешательство в выполняющийся раунд) — только читал схему, не тестировал.

  • -C не тестировал → уже протестировано: когда TUI подключается без -C, cwd потока — это cwd app-server, независимо от того, в какой директории вы запустили codex --remote; с -C <dir> cwd меняется на указанную директорию. (macOS, с запуском TUI через pty, другой клиент читает thread/read.)

  • Весь протокол помечен как [experimental], может измениться при обновлении codex.

Ссылки

Почему обратное направление использует Stop hook, а не другой механизм: docs/reverse-channel.md

Схема протокола: codex app-server generate-json-schema --out <dir>

Maintenance

ActivitySlowing
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    F
    maintenance
    The self-hosted MCP bridge between Claude Chat and Claude Code.
    46
    AGPL 3.0
  • A
    license
    Not graded
    quality
    D
    maintenance
    Connects Claude Desktop and Claude Code, enabling autonomous exchange of messages, files, and code while keeping their context windows separate.
    4
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Bridges Claude Code and OpenAI Codex CLI for an interactive plan-execute-review workflow, enabling Claude to interview, design, and review while Codex implements code changes.
    7
    29 npm
    3
    MIT