Skip to main content
Glama
ar36planet

claude-codex-bridge

by ar36planet

claude-codex-bridge

CI

讓 Claude Code 對 你眼前那個 Codex TUI 說話 —— 而你全程看得到。 反過來,Codex 也能把話送進 Claude Code 正在跑的那個 session。

不是螢幕抓字、不是檔案輪詢、不是 headless subagent。兩邊掛在同一個 codex app-server 的同一條 thread 上: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(package.json 的 engines),建議直接用 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 的 bin 是 #!/usr/bin/env node 的 shim,它跟著 PATH 上的 node 跑, 不綁安裝時的版本。)

架構

        ┌──────────────────────────────┐
        │  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.

所以第二個 client 不是開新對話,也不是把存檔重播一次 —— 是加入同一條正在跑 的 thread。加入之後才收得到那條 thread 的通知串流;只是連上 endpoint 不夠。

Related MCP server: Claude-Gemini MCP Integration Server

用法

三個視窗。

1. 共用 server(開著不要關)

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

--cwd 是 Codex 實際工作的目錄。thread 沒自己指定 cwd 時會沿用 app-server 的,所以不給這個參數就會停在你啟動腳本的地方(也就是 bridge 自己的 資料夾)。也可以用環境變數 CODEX_BRIDGE_CWD。 port 用 --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 要有第一輪對話「跑完」才 resumable,在那之前 thread/resume 會回 no rollout found for thread id。

注意這個陷阱:talk.mjs list 在那之前就已經看得到那條 thread 了 —— TUI 一連 上就會建好 thread 並出現在 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)

只有一條 thread 時 say / read 會自動選它;多條時要 --thread <id> 指定 —— 不猜你在跟哪個 session 講話。list 會一併印出每條 thread 的 cwd,多開時靠這個 分辨。

say 另外收 --cwd <dir>(只改這一輪之後的工作目錄)與 --approvals(見下)。

MCP 介面

CLI 仍可直接使用;MCP 介面提供相同核心能力的結構化 tools。兩個方向共用同一套程式, 但主動發話端各自啟動一個 STDIO process:

  • --role claude:Claude Code 主動對 Codex thread 發話。

  • --role codex:Codex 主動把訊息排入 Claude mailbox。

安裝步驟

0. 先確認前提

  • Node >=22(見開頭的版本說明)。

  • 這個 repo 已經 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 與 Claude 的 Stop hook。

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 只是改設定檔,已經在跑的 session 不會載入新的 MCP server。裝完要把那個 session 關掉重開,tools 才叫得到。

4. 確認裝好了

在重開後的 session 裡叫 bridge_status。ok: true 且 role 正確就成功。 接著 codex_threads_list 應該看得到你的 TUI 那條 thread(含它的 cwd)。

不想開 session 也可以直接從命令列驗證同一條路:

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

Tools

Role

Tools

共用

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 會等整個 turn,因此 Codex 的 MCP 設定建議把 timeout 拉長,並讓 write tools 需要核准:

[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/,回傳有 TTL 的 opaque artifact ID, 再用 bridge_output_read 分頁讀取。單一 capture 預設最多 10 MiB,不會把無上限 reply 塞進 一次 tool result 或 Node heap。

本機驗證:

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,而是照 .bridge.json 連上你眼前那個 TUI,在你正在看的 thread 裡建立一個真的 turn。所以它不在 test:spikes 裡,要自己主動跑。

反方向:Codex → Claude Code

Claude Code 沒有對等的 app-server,沒有 socket 可以推東西進去。它有的是 Stop hook:Claude 要收工前會跑,hook 回 {"decision":"block","reason":...} 就能叫它別停、並把 reason 當成新的輸入繼續做。

所以中間放一個信箱。信箱是具名的 —— 因為可能同時有好幾個 Claude Code session 在聽,共用一個檔案的話誰先收工誰就把別人的信也吞了:

# 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。

這個 repo 的 .claude/settings.json 已經掛好 Stop hook(信箱名 bridge), Claude Code 在這個專案裡收工時會自動把信箱清空並接著做。那個檔案有進版控,所以 你 clone 下來用 Claude Code 打開,這個 hook 就對你生效了 —— 信箱空的時候它完全 沉默,不需要的話刪掉 .claude/settings.json 即可。訊息只會送達一次: drain() 先 rename 再讀,所以同時在寫的人不會被讀到一半。

細節與取捨見 docs/reverse-channel.md。

讓別的 Claude Code session 也用這座橋

server 只要起一份,其他 session 共用。腳本的狀態(.bridge.json、信箱)都是 用模組自身位置解析的,不看 cwd,所以在任何目錄下用絕對路徑呼叫都對。

正方向(那個 session → 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 會印出每條 thread 的 cwd 給你認。 (或者設 CODEX_BRIDGE_URL,就不必依賴 .bridge.json。)

反方向(Codex → 那個 session):要在那個專案的 .claude/settings.json 掛 Stop hook,並給它一個自己的信箱名:

{ "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)自己取,每個 session 一個。

之後 Codex 那側就能指名送信:

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

list 的資料來自每個 session 的 Stop hook 每次執行時的自我登記,所以那個 session 至少要收工過一次才會出現在名單上。

核准(Codex 要動手改東西時)

Claude Code 送進去的 turn 如果要跑指令、改檔案,Codex 會發核准請求。 app-server 把這種請求廣播給所有連上的 client,誰先回誰算數(沒搶到的會收到 serverRequest/resolved)。所以預設策略是 tui:bridge 保持沉默,讓你眼前那 個視窗的提示去決定。

沒人回會卡住整個 turn,所以有保險:超過 CODEX_BRIDGE_APPROVAL_TIMEOUT_MS (預設 300 秒)還沒人回,bridge 自己 fail-closed 拒絕,turn 才能往下走。

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

也可以用 CODEX_BRIDGE_APPROVALS 設預設值。

廣播路由已在 macOS 實測定案(spike-approvals.mjs 11/11):一個 client 按下核准,另一個保持沉默的 client 會收到同一則請求、接著收到 serverRequest/resolved,turn 照常走完。所以「bridge 沉默 = 讓人決定」成立。

會讓「交給 TUI 的人決定」悄悄失效的兩個設定

核准請求在到達任何 client 之前,會先經過使用者自己的 codex 設定。以下兩者任一 生效時,你眼前的 TUI 根本不會被問,bridge 沉默也就不等於人在決定:

設定

位置

效果

PermissionRequest hook

~/.codex/hooks.json

hook 先接走核准請求。macOS 實測:hook 掛著時,兩個 client 一則核准請求都收不到,檔案照樣被寫出來

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 要的是一份(空的) 權限 profile,舊的 execCommandApproval / applyPatchApproval 要的是 {decision:{denied:{rejection}}}。回錯形狀是 schema error,不是禮貌的拒絕。 對照表在 src/appServerWsClient.mjs 的 DEFAULT_SERVER_REQUEST_RESPONSES。

為什麼不是別的做法

做法

問題

wezterm cli send-text / get-text

抓的是 TUI 渲染後的畫面:框線字元、spinner、換行截斷;判斷「回完了沒」只能靠輪詢畫面變化

共用檔案信箱(正方向)

可行但看不到即時狀態,且觸發要人工介入

/codex:rescue subagent

每次冷啟動、獨立 session,接不到你眼前那個 TUI

本方案

結構化事件;turn/steer 還能對執行中的 turn 插話

反方向仍然是檔案信箱 —— 但那是因為 Claude Code 沒有可掛的 socket,而 Stop hook 讓「觸發」不必人工介入。

安全

  • Listener 綁 loopback。--ws-auth 只對 non-loopback 生效,所以本機免 token。

  • 核准預設交給人(tui),逾時 fail-closed。非核准類的 server→client 請求 (工具呼叫、MCP elicitation)一律 fail-closed —— bridge 沒有能問人的 UI。

已驗證 / 未驗證

三個 spike,各自起自己的 app-server(ephemeral port),不會碰到你正在看的 thread:

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 上 渲染成 user message、Codex 正常回覆、回覆串流回 Claude Code。

已釐清(codex-cli 0.147.0):

  • 通知一律帶 threadId,串流類(item/agentMessage/delta)另帶 turnId, 且 turn/start 回傳的 turn.id 與串流上的完全一致。 早期以為「通知不帶 threadId」,實際上是 client 沒 join 到 thread 的症狀。

  • thread 必須先 thread/resume 加入才收得到通知;而 thread 要第一輪對話 跑完才 resumable。

  • historyMode: "paginated"(TUI 建立的 thread)→ thread/read 帶 includeTurns 會失敗(list_turns is not supported yet)。事後撈歷史這條路 目前是斷的;回覆靠即時串流接。

  • 核准會廣播給所有 client,第一個回答的算數(macOS 定案)。沉默的那個 client 同樣收得到請求,事後收到 serverRequest/resolved,turn 不會卡死。 在 OS sandbox helper 起不來的機器上(某些受管企業 Windows 會出現 ShellExecuteExW failed to launch setup helper: 1223),寫檔在「問人」之前就先 失敗,核准請求根本不會發出,所以那三項會標成 SKIP —— 環境限制,不是協定問題。

  • 使用者層的 PermissionRequest hook 會整個攔掉核准請求,client 一則都收不到 (macOS 實測)。細節見上方「核准」章節。

尚未驗證:

  • turn/steer(對執行中的 turn 插話)只讀過 schema,沒實測。

  • -C 沒實跑 → 已實測:TUI 不帶 -C 連上時,thread 的 cwd 是 app-server 的 cwd,跟你在哪個目錄下敲 codex --remote 無關;帶 -C <dir> 才會換成那個目錄。(macOS,用 pty 起 TUI、另一個 client 讀 thread/read。)

  • 整組協定標著 [experimental],codex 升版可能會漂移。

參考

反方向為什麼是 Stop hook、而不是別的機制: docs/reverse-channel.md

協定 schema:codex app-server generate-json-schema --out <dir>

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