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의 의미입니다:

thread_id가 실행 중인 thread를 식별하면, app-server는 해당 thread에 다시 참여합니다.

따라서 두 번째 클라이언트는 새 대화를 시작하거나 저장된 것을 재생하는 것이 아니라 동일한 실행 중인 thread에 참여합니다. 참여한 후에야 해당 thread의 알림 스트림을 수신할 수 있습니다. 단순히 endpoint에 연결하는 것만으로는 충분하지 않습니다.

Related MCP server: Claude-Gemini MCP Integration Server

사용법

세 개의 창.

1. 공유 서버(열어 두고 닫지 마세요)

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

--cwd는 Codex가 실제로 작업하는 디렉터리입니다. thread가 자체 cwd를 지정하지 않으면 app-server의 cwd를 사용하므로, 이 매개변수를 제공하지 않으면 스크립트를 시작한 위치(즉, 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 프로세스를 시작합니다:

  • --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는 허용하지 않습니다. 짧은 응답은 인라인으로; 64 KiB를 초과하면 .bridge-output/에 기록하고, TTL이 있는 불투명한 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을 새로운 입력으로 계속 처리하도록 지시합니다.

따라서 중간에 mailbox를 둡니다. mailbox는 이름이 지정됩니다 — 여러 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(mailbox 이름 bridge)이 연결되어 있습니다. 이 프로젝트에서 Claude Code를 사용할 때, 작업을 마치면 자동으로 mailbox를 비우고 계속 진행합니다. 해당 파일은 버전 관리에 포함되어 있으므로, repo를 clone하고 Claude Code로 열면 이 hook이 자동으로 적용됩니다 — mailbox가 비어 있을 때는 완전히 조용하며, 필요하지 않으면 .claude/settings.json을 삭제하면 됩니다. 메시지는 한 번만 전달됩니다: drain()은 먼저 rename한 후 읽으므로, 동시에 쓰는 사람이 읽는 도중에 가져가지 않습니다.

자세한 내용과 절충 사항은 docs/reverse-channel.md를 참조하세요.

다른 Claude Code session도 이 bridge를 사용하도록 설정

server는 한 번만 시작하면 되며, 다른 session이 공유합니다. 스크립트의 상태(.bridge.json, mailbox)는 모듈 자체 위치를 기준으로 해석되며, 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을 연결하고, 자신의 mailbox 이름을 지정해야 합니다:

{ "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로 절대 경로를 지정해야 합니다. mailbox 이름(위 예제의 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는 이 요청을 연결된 모든 클라이언트에 브로드캐스트하며, 먼저 응답하는 클라이언트의 응답이 적용됩니다(늦은 클라이언트는 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): 한 클라이언트가 승인을 누르면, 침묵을 유지하는 다른 클라이언트는 동일한 요청을 받은 후 serverRequest/resolved를 수신하며, turn은 정상적으로 진행됩니다. 따라서 'bridge 침묵 = 사람이 결정하도록 함'이 성립합니다.

'TUI의 사람이 결정하도록 함'을 조용히 무효화하는 두 가지 설정

승인 요청이 클라이언트에 도달하기 전에, 먼저 사용자 자신의 codex 설정을 통과합니다. 다음 중 하나라도 활성화되어 있으면, 당신 앞의 TUI는 전혀 질문을 받지 않으며, bridge의 침묵이 사람이 결정하는 것과 같지 않습니다:

설정

위치

효과

PermissionRequest hook

~/.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}}}을 요구합니다. 잘못된 형식은 schema 오류지, 예의 바른 거절이 아닙니다. 대조표는 src/appServerWsClient.mjs의 DEFAULT_SERVER_REQUEST_RESPONSES에 있습니다.

왜 다른 방법을 사용하지 않았는가

방법

문제

wezterm cli send-text / get-text

TUI 렌더링 후의 화면을 캡처: 테두리 문자, spinner, 줄바꿈 잘림; '응답 완료' 판단은 화면 변화를 폴링해야 함

공유 파일 mailbox (정방향)

가능하지만 실시간 상태를 볼 수 없고, 트리거에 수동 개입이 필요함

/codex:rescue subagent

매번 콜드 스타트, 독립 session, 당신 앞의 TUI에 연결되지 않음

이 방법

구조화된 이벤트; turn/steer로 실행 중인 turn에 끼어들 수도 있음

반대 방향은 여전히 파일 mailbox입니다 — 그러나 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(임시 포트)를 시작하며, 현재 보고 있는 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가 없다'고 생각했지만, 실제로는 클라이언트가 thread에 join하지 않은 증상이었습니다.

  • thread는 먼저 thread/resume으로 join해야 알림을 받을 수 있습니다; 그리고 thread는 첫 대화가 완료되어야 resumable 상태가 됩니다.

  • historyMode: "paginated"(TUI가 생성한 thread) → thread/read에 includeTurns를 사용하면 실패합니다(list_turns is not supported yet). 사후에 history를 가져오는 이 경로는 현재 막혀 있습니다; 응답은 실시간 스트림으로 받습니다.

  • 승인은 모든 클라이언트에 브로드캐스트되며, 먼저 응답한 클라이언트의 응답이 적용됩니다(macOS 확정). 침묵하는 클라이언트도 동일한 요청을 받고, 나중에 serverRequest/resolved를 수신하며, turn이 멈추지 않습니다. OS sandbox helper를 시작할 수 없는 기기(일부 관리되는 기업 Windows에서 ShellExecuteExW failed to launch setup helper: 1223 발생)에서는 파일 쓰기가 '사람에게 묻기' 전에 실패하여 승인 요청이 전혀 실행되지 않으므로, 해당 항목들은 SKIP으로 표시됩니다 — 환경 제한, 프로토콜 문제가 아닙니다.

  • 사용자 수준의 PermissionRequest hook은 승인 요청을 완전히 가로챕니다, 클라이언트는 하나도 받지 못합니다 (macOS 실험 확인). 자세한 내용은 위 '승인' 섹션을 참조하세요.

아직 검증되지 않음:

  • turn/steer(실행 중인 turn에 끼어들기)는 schema만 읽었고, 실제 테스트되지 않음.

  • -C를 실제로 실행하지 않음 → 실제 테스트 완료: TUI가 -C 없이 연결되면 thread의 cwd는 app-server의 cwd이며, 당신이 어느 디렉터리에서 codex --remote를 입력했는지와 무관합니다; -C <dir>을 사용하면 해당 디렉터리로 변경됩니다. (macOS, pty로 TUI를 시작하고 다른 클라이언트가 thread/read를 읽음.)

  • 전체 프로토콜은 [experimental]로 표시되어 있으며, codex 버전이 업그레이드되면 변경될 수 있습니다.

참고

반대 방향이 Stop hook인 이유, 다른 메커니즘은 아닌 이유: docs/reverse-channel.md

프로토콜 schema: 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