A local MCP-based orchestrator that lets Claude Code (or any MCP client) delegate implementation work to OpenAI Codex, review Codex's output, run verification, send targeted follow-ups, and loop until a project goal passes or a safety limit is hit.
It runs two ways from the same code:
- as an MCP server (official
@modelcontextprotocol/sdk) exposing 5 tools, and - as a thin CLI that drives the identical loop.
It ships with a dry-run / mock mode that proves the whole loop end-to-end (checklist → task → verify → review → follow-up → done) with no real Codex call and no network.
It also ships a ready-made /massa slash command for Claude Code
(commands/massa.md) that drives the whole flow: it
clarifies the goal with you, writes PROJECT_GOAL.md, starts the loop, and
reports progress. See The /massa command.
- Architecture
- Requirements
- Install & build
- Quick start: the dry-run proof
- The 5 MCP tools
- Register in Claude Code (exact config)
- The /massa command for Claude Code
- Worker modes
- Installing & configuring the real Codex CLI
- CLI usage
- The autonomous loop
- Safety guards & limits
- State & logs layout
- Worked end-to-end example
- Codex CLI compatibility
- Troubleshooting
- Runtime: TypeScript / Node (ESM), built with
tsc. Tests use the built-innode:testrunner — no extra test dependency. - Integration path:
codex mcp-serveris the primary worker transport (driven via the MCP SDK client over stdio).codex exec --jsonis the fallback. Both sit behind oneCodexClientinterface alongside a mock impl. - Dependencies (pinned):
@modelcontextprotocol/sdk(mandated SDK) andzod(the SDK's tool-schema API). Dev:typescript,@types/node. Nothing else.
| File | Responsibility |
|---|---|
codex-client.ts |
Codex worker behind one interface: MockCodexClient, McpCodexClient (codex mcp-server), ExecCodexClient (codex exec --json). |
loop.ts |
Loop controller: goal → checklist → milestone → verify → review → follow-up → report. |
verifier.ts |
Reviewer/verifier: runs verification commands, parses git diff, evaluates checklist checks, computes completion. |
guards.ts |
Explicit safety guard checks (run config, outgoing prompt, observed diff). |
store.ts |
Logging + state store: JSON state, per-iteration logs, JSONL event log, checklist.md, final report, stop flag. |
config.ts |
Defaults, limits, guard thresholds, and the version-dependent Codex CLI launch settings. |
types.ts |
Shared TypeScript types / JSON state shapes. |
mcp-server.ts |
MCP server wrapper registering the 5 tools. |
cli.ts |
CLI wrapper driving the same modules. |
- Node ≥ 20 (developed/tested on Node 24).
npm. - For real Codex runs: the OpenAI Codex CLI installed and authenticated (see below). Not required for the dry-run / tests.
# from a clean clone:
npm install # installs pinned deps
npm run build # compiles src/ + test/ to dist/
npm test # builds, then runs the full test suite (13 tests)npm test output ends with pass 13 / fail 0.
Runs the full loop against the bundled sample project using the mock worker — no Codex, no network:
npm run dry-runThis is equivalent to:
node dist/src/cli.js run \
--dry-run \
--project-dir ./examples/sample-project \
--goal-file PROJECT_GOAL.md \
--verify "node greet.test.js"It produces examples/sample-project/.codex-orchestrator/ with the checklist,
per-iteration logs, persisted state, and a final report. See the full captured
output in Worked end-to-end example.
Registered with exactly these names and input names:
Start an autonomous loop; returns a run_id and runs in the background.
| Input | Type | Notes |
|---|---|---|
project_dir |
string | absolute path to the project |
goal_file |
string | default PROJECT_GOAL.md, relative to project_dir |
max_iterations |
int | default 10 |
verification_commands |
string[] | default [] |
sandbox |
enum | read-only | workspace-write | danger-full-access; default workspace-write |
approval_policy |
enum | untrusted | on-failure | on-request | never; default never |
Send one prompt to Codex; returns its output + thread/session id. Outgoing prompts are guard-checked; a blocked prompt is not sent.
| Input | Type | Notes |
|---|---|---|
project_dir |
string | |
prompt |
string | |
thread_id |
string? | continue a prior Codex conversation |
Run git diff + verification, compare to the goal checklist, return remaining
gaps. Read-only; does not start a loop.
| Input | Type |
|---|---|
project_dir |
string |
goal_file |
string (default PROJECT_GOAL.md) |
verification_commands |
string[] |
Return current iteration, latest Codex output, latest verification result, and
the next action for a run_id.
| Input | Type |
|---|---|
run_id |
string |
Request a clean cooperative stop of a run_id.
| Input | Type |
|---|---|
run_id |
string |
Why mode/max-runtime aren't tool inputs: the spec fixes the input names above, so the worker mode and the max-runtime limit are read from environment variables instead (
CODEX_ORCHESTRATOR_MODE,CODEX_ORCHESTRATOR_MAX_RUNTIME_MS). The max-runtime limit is still always enforced.
Add this to your project's .mcp.json (or merge into Claude Code settings).
Replace the absolute path. A copy is in .mcp.json.example:
{
"mcpServers": {
"codex-orchestrator": {
"command": "node",
"args": [
"/ABSOLUTE/PATH/TO/codex-orchestrator/dist/src/mcp-server.js"
],
"env": {
"CODEX_ORCHESTRATOR_MODE": "mcp-server",
"CODEX_ORCHESTRATOR_MAX_RUNTIME_MS": "1800000"
}
}
}
}Equivalent CLI registration:
claude mcp add codex-orchestrator \
-e CODEX_ORCHESTRATOR_MODE=mcp-server \
-e CODEX_ORCHESTRATOR_MAX_RUNTIME_MS=1800000 \
-- node /ABSOLUTE/PATH/TO/codex-orchestrator/dist/src/mcp-server.jsTo try it with no Codex installed, set CODEX_ORCHESTRATOR_MODE=mock.
Run
npm run buildfirst — the config points atdist/src/mcp-server.js.
commands/massa.md is a slash command that turns the raw
MCP tools into a guided flow. Install it by copying it into your Claude Code
commands directory:
# available in every project:
cp commands/massa.md ~/.claude/commands/massa.md
# or per-project:
cp commands/massa.md YOUR_PROJECT/.claude/commands/massa.mdThen, with the MCP server registered (previous section), type /massa (optionally
followed by a one-line idea) in Claude Code. The command:
- routes by size — a small task goes straight to
codex_task, a real build gets the full loop, and "where does the build stand?" usesreview_current_state; - inspects the repo first and only asks what it can't infer (goal, acceptance criteria, verification commands, constraints);
- writes
PROJECT_GOAL.mdwith a machine-checkable checklist and shows it to you before starting; - starts the loop with the safe defaults (
workspace-write, approvalnever) and polls status without spamming; - stops and asks you whenever a safety guard pauses the run — it never uses
danger-full-accessand never rephrases a prompt past a guard.
Selected via CODEX_ORCHESTRATOR_MODE (MCP server) or --mode / --dry-run (CLI):
| Mode | Transport | Use |
|---|---|---|
mock |
none (scripted) | dry-run / tests; applies steps from <project>/mock-plan.json (or an inline plan) |
mcp-server |
codex mcp-server over stdio (MCP SDK) |
primary real path |
exec |
codex exec --json per call |
fallback when MCP transport is impractical |
The orchestrator does not bundle Codex. For real runs, install + authenticate it. (Verified against the current Codex CLI; see Codex CLI compatibility.)
Install (pick one):
npm install -g @openai/codex # npm (provides the `codex` binary)
brew install --cask codex # Homebrew (note: --cask)
curl -fsSL https://chatgpt.com/codex/install.sh | sh # standalone scriptAuthenticate (pick one):
codex login # browser OAuth (ChatGPT sign-in)
codex login status # check
printenv OPENAI_API_KEY | codex login --with-api-key # API key via stdinVerify the surface the orchestrator targets:
codex --help
codex mcp-server --help # primary transport
codex exec --help # fallback transportConfig lives at ~/.codex/config.toml (CODEX_HOME defaults to ~/.codex).
The orchestrator passes cwd, sandbox, and approval-policy per call; you do
not need to pre-configure those.
codex-orchestrator run --project-dir <dir> [--goal-file PROJECT_GOAL.md]
[--verify "<cmd>" ...] [--max-iterations N]
[--sandbox workspace-write] [--approval-policy never]
[--max-runtime-ms N] [--mode mcp-server|exec|mock] [--dry-run]
codex-orchestrator review --project-dir <dir> [--goal-file ...] [--verify "<cmd>" ...]
codex-orchestrator task --project-dir <dir> --prompt "<text>" [--thread-id <id>] [--mode ...]
codex-orchestrator status --project-dir <dir> --run-id <id>
codex-orchestrator stop --project-dir <dir> --run-id <id>
--verify is repeatable. --dry-run forces --mode mock. Run run exits 0
on completion, 2 on a limit, 3 when paused for approval, 4 when stopped,
1 on error.
Examples:
# Foreground loop against a real project with Codex:
node dist/src/cli.js run --project-dir /path/to/app \
--verify "npm test" --verify "npm run build" --max-iterations 8
# One-shot review (no loop):
node dist/src/cli.js review --project-dir /path/to/app --verify "npm test"Implemented exactly as specified (src/loop.ts):
- Read
PROJECT_GOAL.md. - Read relevant repo docs / manifests / existing tests.
- Produce an acceptance checklist →
.codex-orchestrator/checklist.md. - Ask Codex to implement the smallest coherent milestone.
- Run the configured verification commands.
- Review
git diffagainst the checklist. - If incomplete, send a targeted follow-up: what passed, what failed (exact commands/exit codes/errors), remaining gaps, what to fix next.
- Repeat until completion or a max-iteration / max-runtime limit.
- Produce a final report.
Guards are explicit checks (src/guards.ts), evaluated at three points:
- Run config —
sandbox = danger-full-accessblocks;danger-full-access+neveris refused outright (validateSafetyConfig). - Outgoing prompt — blocks intents to deploy to production, purchase paid
services, access/exfiltrate secrets, weaken auth/security, mass-delete, or use
danger-full-access. - Observed diff — blocks changes outside the project root, edits to
secret/credential files (
.env,*.pem,~/.ssh, …), and large deletions (> 5 files or > 500 lines by default).
A block pauses the loop with status paused_for_approval and a
pending_approval record — it does not proceed without a human.
Hard limits (both always enforced, so the loop can never run forever):
- Max iterations — default 10 (
max_iterations). - Max runtime — default 30 min (
CODEX_ORCHESTRATOR_MAX_RUNTIME_MS/--max-runtime-ms). Per-Codex-call and per-verification timeouts also apply.
Defaults: sandbox workspace-write, approval never (valid only inside
the workspace confinement).
Everything lives under <project>/.codex-orchestrator/:
.codex-orchestrator/
├── checklist.md # spec-mandated path (latest run)
└── runs/<run_id>/
├── state.json # persisted RunState (source of truth)
├── checklist.md # per-run copy
├── events.jsonl # append-only structured event log
├── final-report.md
├── stop.flag # present => cooperative stop requested
└── iterations/
├── iter-001.json / iter-001.md
└── ...
The JSONL event log records every prompt_sent, codex_response,
verification, review, guard, decision, paused, and run_finished.
The bundled examples/sample-project/ has a goal (implement greet()), a
verification test (node greet.test.js), and a mock-plan.json whose first step
writes a wrong greeting (so verification fails and a follow-up is sent) and
whose second step fixes it.
$ npm run dry-run
▶ run run-64cd35db (mode=mock, sandbox=workspace-write, approval=never)
📋 checklist: 3 item(s) → .../examples/sample-project/.codex-orchestrator/checklist.md
── iteration 1/10 ──
→ codex (mock) prompt sent (2953 chars)
← codex responded (95 chars)
🧪 verification: 0/1 passed
✔ review: 33% checklist, 3 gap(s)
── iteration 2/10 ──
→ codex (mock) prompt sent (981 chars)
← codex responded (104 chars)
🧪 verification: 1/1 passed
✔ review: 100% checklist, COMPLETE
🏁 COMPLETED — all checklist items satisfied and verification passed
Final status: completed — all checklist items satisfied and verification passed
State dir: .../examples/sample-project/.codex-orchestrator/runs/run-64cd35db
Checklist: .../examples/sample-project/.codex-orchestrator/checklist.md
Final report: .../examples/sample-project/.codex-orchestrator/runs/run-64cd35db/final-report.mdResulting checklist.md:
# Acceptance Checklist
- [x] **greet-file** — src/greet.js exists and exports greet()
- evidence: file exists: src/greet.js
- [x] **greet-impl** — greet() returns a 'Hello, ...' greeting
- evidence: file src/greet.js matches /Hello/
- [x] **tests-pass** — the greet test passes
- evidence: command passed: node greet.test.jsResulting final-report.md (iteration log):
## Iteration log
- Iteration 1: continue — 3 gap(s) remain; sending a follow-up (verification 0/1)
- Iteration 2: done — all checklist items satisfied and verification passed (verification 1/1)Running it for real (with Codex): drop the --dry-run flag and ensure Codex
is installed/authenticated. The loop will use codex mcp-server, threading the
conversation across iterations via the returned threadId. To exercise the real
git diff review on the sample, git init the target first:
cd examples/sample-project && git init -q && git add -A && git commit -qm baseline && cd -
node dist/src/cli.js run --project-dir ./examples/sample-project --verify "node greet.test.js"The Codex flag/tool surface drifts between versions. All version-dependent names
are centralized in config.ts (DEFAULT_CODEX_CLI) so they can
be adjusted in one place. As built (verified against the current Codex CLI):
- Launch MCP:
codex mcp-server(over stdio). Notcodex mcp serve(codex mcpis the separate "manage external servers" command). - MCP tools:
codex(start) andcodex-reply(continue). Thecodextool takes kebab-case args (prompt,cwd,sandbox,approval-policy) and rejects unknown fields;codex-replytakes camelCase{ threadId, prompt }. - Session id: returned in
structuredContent.threadId; reused asthreadId. - exec fallback:
codex exec --json "<prompt>"; resume withcodex exec resume <id>. The JSONL stream is tagged by dottedtype(thread.startedcarriesthread_id; the final answer is in anitem.completedwhoseitem.type === "agent_message", fieldtext). A legacyEventMsgfallback parser is included. - Sandbox values:
read-only|workspace-write|danger-full-access. - Approval values:
untrusted|on-failure|on-request|never. - Guarded:
--dangerously-bypass-approvals-and-sandbox/--yoloand--sandbox danger-full-accessare never emitted.
If a future Codex renames a tool or flag, edit DEFAULT_CODEX_CLI — no other
code changes are needed.
- "Goal file not found" — pass
--project-dirpointing at the dir that containsPROJECT_GOAL.md, and--goal-filerelative to it. get_loop_statussays "Unknown run_id" — for cross-process lookups the server scans the server's cwd. Launch the MCP server from the project dir, or use the CLIstatuscommand with--project-dir.- Real Codex run errors immediately — run
codex login statusandcodex mcp-server --help; confirm the binary is onPATH(override with theCODEX_BINenv var). - Git diff shows "(not a git repository)" — the diff-based guards/review need
a git repo;
git initthe target. The checklist + verification still work without git. - Loop paused unexpectedly — check
get_loop_status→pending_approval; a safety guard tripped. Review the change, then re-run with adjusted scope.