cc-codex-mcp
# cc-codex-mcp
A local stdio MCP server that lets Claude Code agents and subagents ask **OpenAI Codex** for an independent opinion
(typically a read-only review of their changes) and follow up on the same Codex thread later.
- `codex_ask`: start a Codex thread in a project directory and get the answer plus a `thread_id`.
- `codex_followup`: continue that thread ("I fixed 1 and 3, re-check").
- Per call: `model`, `effort`, `fast`, `sandbox`, `timeout_sec`.
- Runs in an **isolated `CODEX_HOME`** (`~/.codex-reviews`), so these threads never show up in the Codex desktop app.
- Safe for many parallel callers: one process and temp dir per call, no shared state, plus a cross-process lock per thread.
- Windows first (tested on Windows 10 with the Codex desktop app and codex-cli 0.163). macOS/Linux should work but are untested.
## Requirements
- Node.js >= 20.3
- The Codex CLI. On Windows it ships with the Codex desktop app; a separately installed `codex` works too (see
[How Codex is found](#how-codex-is-found)).
- A ChatGPT account that can use Codex.
## Install
```bash
git clone https://github.com/NtTestAlert/cc-codex-mcp.git
cd cc-codex-mcp
npm install
npm run build
```
### 1. One-time login for the isolated home
The isolated home has its own login. `auth.json` is **never** copied or linked from `~/.codex`, because token
refreshes rewrite that file.
Windows (PowerShell):
```powershell
powershell -ExecutionPolicy Bypass -File scripts\login.ps1
# or by hand:
$env:CODEX_HOME="$HOME\.codex-reviews"; & "$env:LOCALAPPDATA\Codex\versions\current\resources\codex.exe" login
```
Add `-DeviceAuth` (or `--device-auth`) if no browser can open. On macOS/Linux:
`CODEX_HOME="$HOME/.codex-reviews" codex login`.
Until you log in, every tool call returns an error containing this command.
### 2. Register with Claude Code (CLI and desktop app)
Claude Code's CLI and the Claude desktop app (Code tab) share the same user-scope MCP config (`~/.claude.json`), so
one registration covers both. Use an absolute path to a real `node.exe`: version-manager shims (Volta, nvm-windows)
can pick a different Node per project, and Volta's shim also mangles `<`/`>` in arguments.
```bash
claude mcp add-json --scope user cc-codex-mcp '{"type":"stdio","command":"C:/path/to/node.exe","args":["C:/path/to/cc-codex-mcp/dist/index.js"],"timeout":3700000}'
claude mcp get cc-codex-mcp
```
`claude mcp get` should show `Status: ✔ Connected`. New Claude Code sessions (CLI or desktop) pick it up; sessions
that were already open need a restart. Tools appear as `mcp__cc-codex-mcp__codex_ask` and
`mcp__cc-codex-mcp__codex_followup`.
Other MCP clients (Claude Desktop chat, Cursor, etc.) use the usual stdio entry: command = absolute `node`,
args = `[".../dist/index.js"]`.
#### Tool-call timeout
Claude Code has two limits on MCP tool calls (checked in Claude Code 2.1.277 / desktop 2.1.299):
- a hard wall-clock limit: the per-server `"timeout"` (ms) if set, otherwise `MCP_TOOL_TIMEOUT`, otherwise about 28 h;
- an idle limit, `CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT` (default 30 min for stdio), which progress notifications reset.
The per-server `"timeout": 3700000` above covers the maximum `timeout_sec` (3600). It works in both the CLI and the
desktop app, regardless of environment-variable precedence (under the desktop app, a variable already in the host's
environment wins over `settings.json` `env`). The server sends a progress notification every ~15 s while Codex runs,
which keeps the idle limit from firing. You don't need to set `MCP_TOOL_TIMEOUT`; setting it would *lower* the 28 h
default for every server.
### 3. Tell your agents when to use it
See [`CLAUDE.snippet.md`](CLAUDE.snippet.md): drop it into a project's `CLAUDE.md`. Implementer subagents then get a
read-only review when they finish, fix real issues, re-check on the same thread, stop after 3 rounds, and never use
fast mode unless told to.
### Check the setup
```bash
node dist/index.js --doctor
```
This prints the resolved Codex executable, the isolated home and login state, and the MCP sync report: each server,
its transport, and whether `default_tools_approval_mode` is set. It also creates the isolated home if needed and runs one real config
sync, so it may update the isolated `config.toml` (never `~/.codex`).
## Tools
### `codex_ask`
| input | default | |
|---|---|---|
| `prompt` | required | sent on stdin, so no quoting issues |
| `cwd` | required | project directory (`-C`) |
| `sandbox` | `read-only` | or `workspace-write` |
| `model` | config default | `-m` |
| `effort` | config default | `low\|medium\|high\|xhigh\|max\|ultra` → `-c model_reasoning_effort="…"`; checked against the model's supported levels |
| `fast` | `false` | `-c service_tier="fast" -c features.fast_mode=true`; uses about **2.5x** the subscription limits |
| `skip_git_check` | `false` | `--skip-git-repo-check` |
| `timeout_sec` | 900 | 10…3600; on timeout the whole process tree is killed (`taskkill /T /F`) |
Runs `codex exec --json --color never -C <cwd> -s <sandbox> [...] -o <temp file> -`.
### `codex_followup`
Inputs: `thread_id` (a UUID, validated, and it must exist in the isolated home), `prompt`, plus optional `model`,
`effort`, `fast`, `timeout_sec`. Runs `codex exec resume --json --skip-git-repo-check -c sandbox_mode="<original>"
[...] -o <temp file> <thread_id> -`, with the process working directory set to the thread's original cwd. If that directory no longer exists, it runs
from your home directory and says so in `warnings`.
### Result (both tools)
```json
{
"thread_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"answer": "…",
"duration_sec": 7.5,
"warnings": [],
"activity": { "commands": 2, "mcp_tool_calls": ["openaiDeveloperDocs.search_openai_docs (completed)"], "subagent_events": 0 },
"error": "…only when something failed…",
"answer_file": "…only when the answer was truncated…"
}
```
- `answer` is the `-o` file (falling back to the last `agent_message`), ANSI-stripped and capped at 50,000 characters.
When it's longer, the full text stays in `answer_file`.
- `warnings` collects MCP server startup failures, failed or rejected MCP tool calls, declined commands, unsupported
service tier / fast-mode messages, config problems, and config-sync errors.
- On `turn.failed`, `error` events in a turn that didn't complete, a non-zero exit, a timeout or cancellation, `error` holds the message plus the last
40 stderr lines. Any partial answer and the `thread_id` are still returned.
- `activity` is only present when Codex ran commands, called MCP tools, or spawned subagents.
## Environment variables
| variable | default | |
|---|---|---|
| `CODEX_BRIDGE_HOME` | `~/.codex-reviews` | isolated `CODEX_HOME`; refused if it resolves to the main home |
| `CODEX_BRIDGE_SOURCE_HOME` | `~/.codex` | main Codex home (source of truth for config and sandbox state) |
| `CODEX_BRIDGE_CODEX_PATH` | auto | explicit Codex executable (`.exe`, `.js`, or an npm `.cmd`/`.ps1` shim to unwrap) |
| `CODEX_BRIDGE_COPY_CONFIG` | unset | `1` = copy `~/.codex/config.toml` once if the isolated home has none |
| `CODEX_BRIDGE_MCP_EXCLUDE` | unset | comma-separated MCP server names not to sync |
| `CODEX_BRIDGE_SYNC_DESKTOP_ONLY` | unset | `1` = also sync desktop-only servers such as `node_repl` |
## How Codex is found
1. `CODEX_BRIDGE_CODEX_PATH`.
2. `codex` on `PATH`. An npm `codex.cmd` shim is unwrapped to the native `codex.exe` inside the npm package, or to
`bin/codex.js` run with the current Node. A shell is never used.
3. The desktop app: `%LOCALAPPDATA%\Codex\versions\current\resources\codex.exe` (`current` is a junction to the
newest version), else the highest `versions\<x.y.z.w>` folder. On macOS, `/Applications/Codex.app/Contents/Resources/codex`.
## What is shared with the Codex desktop app
The desktop app's `~/.codex` is the source of truth. The bridge only **reads** it and only writes the isolated home.
**Config sync** (`config.toml`): at startup, then when `~/.codex/config.toml` changes (`fs.watch`, debounced 1 s),
plus a cheap mtime/size check before each run in case the watcher missed an event. The sync copies:
- the `[mcp_servers.*]` tables (per server name the source wins; servers only in the isolated config are kept;
servers removed from the source are removed again);
- top-level `model` and `model_reasoning_effort`;
- the `[windows]` table. Without `windows.sandbox`, Codex on Windows blocks every shell command in read-only mode,
even `git diff`.
All other isolated-config keys are kept. Writes are atomic and only happen when the synced subset actually changed.
The file is parsed with a real TOML parser (smol-toml), so comments in the isolated config are not preserved.
Desktop-only servers are skipped by default (`node_repl` needs the desktop app's named pipe and hard-codes
`CODEX_HOME=~/.codex`).
**Elevated Windows sandbox state.** The elevated sandbox runs commands as two machine-wide local accounts
(`CodexSandboxOffline` / `CodexSandboxOnline`). Each `CODEX_HOME` keeps its own DPAPI-encrypted copy of their
passwords (`.sandbox-secrets/sandbox_users.json`) and a `.sandbox/setup_marker.json`. If two homes each provision,
every provisioning rotates the passwords and makes the *other* home re-run the elevated setup, endlessly. So the
desktop app's home owns this state, and the bridge copies those two files into the isolated home at startup and
whenever the desktop app rewrites them (`fs.watch`). Nothing happens per call. The isolated home never provisions
while the desktop app has a provisioned sandbox, and `~/.codex` is never written.
Normal Codex behaviour is unaffected: a quick, non-elevated `codex-windows-sandbox-setup.exe` "setup refresh" still
runs before sandboxed commands, as it does in the desktop app.
**Not shared:** `auth.json` (separate login), MCP OAuth tokens (see below), sessions/threads, history, memories.
### Tool approvals for synced MCP servers
`node dist/index.js --doctor` lists every synced server and whether it sets `default_tools_approval_mode = "approve"`.
The bridge never changes that setting. In testing, read-only MCP tools ran fine under `exec` without it. If a server's
tool calls get rejected for approval, `warnings` says so; add `default_tools_approval_mode = "approve"` to that server
in `~/.codex/config.toml` and the sync carries it over.
### MCP OAuth tokens
With Codex 0.163 on Windows, OAuth tokens for remote MCP servers are stored **per `CODEX_HOME`**:
`CODEX_HOME\secrets\mcp_oauth.age`, with its key in Windows Credential Manager under an account derived from the
`CODEX_HOME` path. The isolated home does not see the desktop app's tokens. To log a server in for the isolated home:
```powershell
$env:CODEX_HOME="$HOME\.codex-reviews"; & "$env:LOCALAPPDATA\Codex\versions\current\resources\codex.exe" mcp login <server>
```
Remote plugins enabled on your ChatGPT account are installed into every home, including the isolated one, so their
OAuth MCP servers need this login too. Until a server is logged in, each run carries a harmless `AuthRequired`
warning for it. Servers that authenticate with headers or `bearer_token_env_var` in `config.toml` need nothing extra.
## Verified against the installed CLI (codex-cli 0.163.0-alpha.10.2)
- `exec`: `--json`, `-o`, `-C`, `-s read-only|workspace-write|danger-full-access`, `-m`, `-c key=value` (TOML value),
`--skip-git-repo-check`, and `-` / no prompt = read stdin all exist. The first JSON event is
`{"type":"thread.started","thread_id":…}`. Thread ids are UUIDv7.
- `exec resume`: accepts `-c`, `-m`, `--json`, `-o`, `--skip-git-repo-check` and `-` for stdin. It has **no `-C`
and no `-s`**, and it runs in the **process cwd**, not the thread's original cwd. So the bridge spawns it in the
original cwd (read from the rollout file) and keeps the sandbox with `-c sandbox_mode="…"`.
- Fast mode: `-c service_tier="fast"` is accepted and recorded as `service_tier: "priority"`. An unsupported tier
produces an `error` item ("…not advertised as supported for model…"), which lands in `warnings`.
- Reasoning effort key: `model_reasoning_effort`. Supported levels come from Codex's own model list
(`<CODEX_HOME>/models_cache.json`); at the time of writing the models offered `low, medium, high, xhigh, max` and
most also `ultra`, and none offered `minimal`. Before starting Codex, the bridge checks `effort` against
the effective model: the `model` input, else the thread's model for follow-ups, else the configured default. An
unsupported level is rejected with the supported list. Unknown models are passed through unchecked. Verified live:
`low`, `xhigh`, `max` and `ultra` are each recorded as the turn's `reasoning_effort`.
- Subagents (`multi_agent`, stable) work under `exec`: the child thread has `thread_source: "subagent"` and the
parent's `parent_thread_id`.
## Known limitations
- Occasionally a run sits for several minutes waiting on the model API (seen: 225 s and ~438 s for a one-word
reply, both with and without fast mode; the next identical runs took ~7 s). The default 900 s timeout covers it.
- Settings this Codex version doesn't recognise produce a warning on every run until they're removed from
`~/.codex/config.toml`.
- On Windows, if `codex.exe` itself crashes while a grandchild still holds its pipes, that grandchild can outlive it
(Node can't put children in a Job Object). Timeouts, cancellation and shutdown kill the whole tree.
- The thread lock is a lockfile with a 30 s heartbeat. A crashed holder is reclaimed after its PID is gone, or after
2 minutes without a heartbeat.
- macOS/Linux paths are implemented but untested.
## Development
```bash
npm test # build + node:test (unit tests use temp dirs and a fake codex)
node dist/index.js --doctor
```
Layout: `src/index.ts` (MCP tools), `runner.ts` (spawn, JSONL parsing, timeout and tree kill), `codexPath.ts`,
`lock.ts`, `configSync.ts`, `sandboxMirror.ts`, `home.ts`, `sessions.ts`, `util.ts`.
TDQS
Scored across 2 tools
codex_ask starts a new independent-review thread, while codex_followup continues an existing one via thread_id. The two purposes are cleanly separated with no overlapping scope, so an agent can choose reliably.
Both tools use the same codex_ prefix with snake_case action suffixes (ask, followup). The pattern is predictable and no conventions are mixed.
Two tools is lean but well-matched to a narrow purpose: initiate an opinion thread and extend it. It is slightly below the typical 3-15 range, but each tool clearly earns its place.
The ask/follow-up pair covers the core lifecycle of getting and continuing an independent review. Minor gaps such as listing or cancelling threads exist but are not required for the stated workflow.