Reviewer drivers
This page is for contributors. It records how openqodex review starts its own reviewer process, and what was observed with the real binary before a driver was enabled. A driver is enabled only when its isolation was shown by a real run. A limit that no setting removes is stated below and in the user docs.
The reviewer is a separate coding-agent process that openqodex starts, without a window, for one review. It reads a frozen copy of the change (the snapshot) and answers with one JSON object. The trace is the agent's own event stream: every tool call, its input and whether it succeeded.
Claude Code
Tested with Claude Code 2.1.289 (claude --version) on macOS, 2026-10-03, on a throwaway folder and on the demo repo.
The command line
The driver starts this command without a shell, with the snapshot as the working directory, and writes the brief to standard input:
claude -p --output-format stream-json --verbose --input-format stream-json
--tools Read,Grep,Glob
--permission-mode dontAsk
--setting-sources ""
--settings {"autoMemoryEnabled":false,"hooks":{},"disableAllHooks":true}
--strict-mcp-config --mcp-config {"mcpServers":{}}
--disable-slash-commands
--no-session-persistence
The child gets an environment built from an allowlist (reviewerEnv in packages/review/src/agents/claude.ts): PATH, HOME, USER, LOGNAME, SHELL, TMPDIR, locale, TERM, TZ, CLAUDE_CONFIG_DIR, proxy and CA settings, the ANTHROPIC_* key, URL and model variables, and the Bedrock, Vertex or Foundry variables only when the matching CLAUDE_CODE_USE_* flag is set; plus OPENQODEX_REVIEW_DEPTH=1. No other variable is copied, so no developer token and nothing that ties the child to a running Claude Code session (CLAUDECODE, CLAUDE_CODE_SESSION_ID, messaging sockets) reaches it. A run from inside a Claude Code session with this environment worked as the runs below did.
What each flag was observed to do
| Flag | Observed |
|---|---|
-p with --input-format stream-json |
A fresh session that reads user messages as JSON lines on standard input. After each answer it prints a result event and waits for the next message, so a correction round goes to the same session. Closing standard input ends the process with exit 0. |
--output-format stream-json --verbose |
One JSON event per line: an init event (tools, MCP servers, plugins, permission mode, memory paths, version), every tool_use with its input, every tool_result with is_error, a permission_denied event for each refusal, and a result event with the final text, turns, usage and cost. For a Read, tool_use_result.file gives the path, startLine and numLines delivered. |
--tools Read,Grep,Glob |
The init event lists exactly Glob, Grep, Read. Asked to run ls / and to write a file, the model answered that it has no Bash or Write tool; no such tool call appears in the trace. The Agent tool is absent, so no subagent can start. |
--permission-mode dontAsk |
Reads inside the working directory succeed. A Read of an absolute path outside it (/tmp/.../outside/secret.txt, /etc/hosts, a decoy ssh config), a relative path that leaves it (../outside/secret.txt), a Read through a link inside the folder that points outside, and a Grep or Glob rooted outside (/tmp/..., /) were each refused with a permission_denied event and an error result. A recursive Grep and a **/* Glob in the folder did not follow the link out. |
--tools Read,Grep,Glob,WebSearch,WebFetch --allowedTools WebSearch,WebFetch (the default; dropped with reviewer_web: off or review --reviewer-web off) |
The init event lists the five tools. Without --allowedTools, dontAsk refused both web tools ("Permission to use WebFetch has been denied because Claude Code is running in don't ask mode"); with it, a WebFetch of example.com and a WebSearch both returned results (2026-10-03). |
--setting-sources "" |
No user, project or local settings file is read. In the same folder, a run without this flag loaded the project CLAUDE.md canary (the answer ended with the canary word) and the user's global instructions (the answer quoted them, about 155,000 input tokens); with it, the input was about 4,500 tokens and neither canary nor any sentence of the global file appeared anywhere in the event stream. With --include-hook-events, a run reading user settings showed 11 hook events; this run showed none. |
--settings {"autoMemoryEnabled":false,"hooks":{},"disableAllHooks":true} |
The init event has no memory_paths: auto memory is off. With disableAllHooks, a SessionStart hook that a terminal wrapper (cmux) added to every claude it starts no longer ran: 2 hook events without it, 0 with it (2026-10-04, Claude Code 2.1.289). The driver also stops the run if any hook event appears in the stream. A repository AGENTS.md with a canary instruction was not followed. |
--strict-mcp-config --mcp-config {"mcpServers":{}} |
The init event lists no MCP server. |
--disable-slash-commands |
The init event lists no skill and no slash command. |
--no-session-persistence |
Nothing is saved for a later --resume; the correction rounds use the open process instead. |
AGENTS.md: a canary there was not loaded in any run, with or without settings. The built-in plugins (cc-plugin-agents-md, cc-plugin-telemetry, cc-plugin-plugin-authoring) stay listed; they are part of Claude Code and read no repository instruction file in this configuration.
Where it works
- Started from a Bash tool inside a running Claude Code session: works, with the same tools and the same refusals.
- Started from a plain environment (
PATH,HOME,USER,LOGNAME,TMPDIRand the developer's ownCLAUDE_CONFIG_DIR): works. WithoutUSERthe login in the macOS keychain is not found and the run ends with "Not logged in". - A temporary
CLAUDE_CONFIG_DIRloses the login, so the driver keeps the developer's own configuration folder and excludes its contents with the flags above.
Detecting it
claude --versionprints the version.claude auth statusprints JSON withloggedIn. Exit 1 and"loggedIn": falsemean the reviewer cannot start.
Usage
Each result event carries num_turns and usage for that turn (input, output and cache tokens), and total_cost_usd and modelUsage for the session so far. The driver adds up the turns and takes tokens and cost from the last result event.
The boundary and the alarm
The boundary is Claude Code's own permission rules: --tools Read,Grep,Glob and --permission-mode dontAsk with no settings source, which refused every read outside the working folder in the runs above. The alarm is the tool's own check of the event stream (packages/review/src/agents/trace.ts), which does not trust the boundary and fails closed:
- the run fails when the
initevent lists any tool beyond Read, Grep and Glob, any MCP server or a memory path; theAgenttool is never listed, so no subagent or nested turn can make a call the stream does not show, and everytool_usein the stream is checked whichever turn it came from; - every tool call counts from the moment the agent asks for it, with or without a result; a tool name other than the three makes the review incomplete;
- every path-bearing input (
file_path,path,notebook_path,cwd,directory, and apatternorglobthat starts at/,~, a drive or..) is resolved against the snapshot, then through the real path of its deepest existing folder, and compared case-insensitively on macOS and Windows; a path with$,%or a NUL is refused,~is the home folder; - an input that is not an object, or a path field that is not text, makes the review incomplete;
- any attempt outside the snapshot makes the review incomplete, even one the agent refused;
- one place outside the snapshot is the agent's own. Claude Code saves a tool result too large to hand the model at
<configuration folder>/projects/<its name for the working folder>/<session id>/tool-results/<tool call id>.txtand tells the model to read it back. A call on thetool-resultsfolder of this session (the session id from theinitevent; the configuration folder fromCLAUDE_CONFIG_DIR, else~/.claude, asinitreads it) is kept intrace.jsonwithown: trueand does not make the review incomplete: it holds only what this session's own calls returned, and each of those calls was checked. Any other path in the configuration folder (the login, another session's output, a transcript) stays outside. With Claude Code 2.1.296,dontAskrefused that read (2026-10-09).
The snapshot holds no links (they are written as plain files) and secrets the scanners found are redacted in every file of it before the reviewer starts; a file too large to check is removed from it.
What the agent stores
With --no-session-persistence and auto memory off, real runs with Claude Code 2.1.289 left no transcript, no history.jsonl line and no project entry for a snapshot folder in the configuration folder (searched for the brief's text and the snapshot paths after the runs). A tool result too large to hand the model is the exception: Claude Code 2.1.296 saved it at projects/<snapshot folder name>/<session id>/tool-results/ in the configuration folder, and it stays there after the run (2026-10-09). An earlier run without autoMemoryEnabled: false left one empty projects/<folder>/memory folder; with the flag, none. The driver keeps the developer's configuration folder because a temporary one loses the login.
Not covered
- Managed (policy) settings set by an organisation still apply; they can add hooks or permission rules. The trace check above still fails a run that reads outside the snapshot.
- Each new Claude Code version can change these flags. Re-run these checks before raising the tested version.
Codex
Enabled since 0.6.0, with two stated limits. Tested with codex-cli 0.160.0 (/opt/homebrew/bin/codex --version) on macOS, 2026-10-03 and 2026-10-04, logged in with a ChatGPT account, on throwaway folders under ~/.openqodex/ with canaries. The driver is packages/review/src/agents/codex.ts. It refuses a Codex older than 0.160.0, and a Codex whose --version prints no version number.
The command line
The driver starts this command without a shell, with the snapshot as the working directory, and writes the prompt to standard input. Its environment comes from an allowlist (codexEnv): PATH, HOME, USER, LOGNAME, SHELL, TMPDIR, locale, TERM, TZ, CODEX_HOME, CODEX_CA_CERTIFICATE, SSL_CERT_FILE and proxy settings, plus OPENQODEX_REVIEW_DEPTH=1. No OPENAI_API_KEY, no CODEX_THREAD_ID and no CODEX_SANDBOX reach it. Only absolute PATH entries are kept: an empty or relative one would resolve inside the snapshot.
codex exec --json --color never --ephemeral --skip-git-repo-check
--ignore-user-config --ignore-rules
-C <snapshot>
-c approval_policy="never"
-c default_permissions="openqodex_review"
-c permissions.openqodex_review.filesystem={":minimal"="read",":project_roots"="read","/tmp"="deny"}
-c web_search="disabled" (web_search="cached" unless reviewer_web: off)
-c project_doc_max_bytes=0
-c allow_login_shell=false
-c shell_environment_policy.inherit="core"
-c skills.include_instructions=false -c skills.bundled.enabled=false
--disable plugins --disable apps --disable hooks --disable multi_agent --disable memories
--disable browser_use --disable computer_use --disable image_generation --disable skill_search
--disable tool_suggest --disable goals --disable in_app_browser --disable view_image
-
codex sandbox -c ... -- <command> runs one command under the same sandbox without a model, and codex debug prompt-input -c ... prints what the model would be given; both cost nothing and were used for most checks below.
What each part was observed to do
| Part | Observed |
|---|---|
exec --json |
One JSON event per line: thread.started, turn.started, item.started and item.completed for messages, commands and web searches, turn.completed with usage (input_tokens, cached_input_tokens, cache_write_input_tokens, output_tokens, reasoning_output_tokens). cached_input_tokens is part of input_tokens. A run can print more than one agent_message; the last one is the answer. The run ends after one answer. |
--ephemeral |
No rollout file was written for the test folder. A follow-up in the same session is not possible, so each correction round is a new run that carries the brief, every earlier answer and every earlier correction, in order. A subagent cannot start: spawn_agent failed with "no rollout found for thread id". |
-s read-only alone |
Writes and network were refused, but every read was allowed: cat of a file in another /tmp folder, of /etc/hosts and ls ~/.ssh all succeeded. So the driver does not use it. |
the openqodex_review permission profile |
Reads outside the folder were refused (Operation not permitted): a file in a home folder outside it, ~/.ssh, ~/.codex, ~/Projects, $TMPDIR, and /tmp with the /tmp deny entry. Reads inside it worked. /etc and the system folders :minimal names stay readable; without :minimal no command could start. touch was refused. curl could not resolve a host. The patch tool was refused ("writing is blocked by read-only sandbox"). Programs outside those folders do not start: in a real review rg was "command not found", and the model used ls, find and sed. |
web_search="disabled" |
The model reported no web search tool. |
web_search="cached" |
Asked to search, the model ran one search; the stream showed a web_search item with the query and its results (2026-10-04). Shell commands still had no network: the profile has no network entry. |
project_doc_max_bytes=0 |
A canary AGENTS.md in the folder did not appear in the prompt input or the answer. |
skills.include_instructions=false |
Without it, a canary skill in the folder's .agents/skills/ was listed to the model, which then followed it. With it, the skills block is gone from the prompt input. |
--ignore-user-config |
The developer's config.toml (MCP servers, plugins, model, trusted projects) is not read. A canary developer_instructions in the folder's .codex/config.toml did not appear either way: the folder is not a trusted project. |
The per-run probe of the boundary
The read confinement rests on two -c keys (default_permissions and permissions.openqodex_review.filesystem). A newer Codex could rename or ignore them, and the event stream would not show it. So every review proves the boundary before the first model run (probeSandbox). It runs one command, with no model, under codex sandbox with the same two keys, in the snapshot folder:
- it reads a canary file that openqodex writes in its home folder (
~/.openqodex/.openqodex-probe-<random>, mode 0600, random content), outside the snapshot; - it reads a file with random content that openqodex writes inside the snapshot;
- it tries to create a file inside the snapshot;
- it prints a random marker as its last act.
The script runs every program by absolute path (/bin/cat) with PATH=/usr/bin:/bin, so a program committed in the snapshot cannot stand in for one. It handles each expected refusal itself, so the marker prints only when every step ran. The review starts only when the probe exits 0 with no signal, the marker came back, the inside read worked, the canary's content did not come back and no file was created. Any other result, a probe that cannot start, or one that runs past 30 seconds ends the run as "Full review unavailable" with "Codex's sandbox did not confine reads to the review copy; the review did not start" and the review --agent fallback. The canary and both probe files are removed whatever happened, before the snapshot is hashed. A Ctrl-C or a kill during the probe ends the probe's process group and removes the files before review exits. codex sandbox passes on the command's exit status (3 for exit 3, 137 for a killed shell).
Observed with codex-cli 0.160.0 (2026-10-04):
- With the review profile,
catof the canary printed "Operation not permitted",catof the inside file printed its content, and the write printed "Operation not permitted". - With a profile that adds
"/"="read", the canary's content came back, so the probe refuses it.packages/cli/test/codex-stream.test.tsruns both against the real binary when Codex is installed (skipped in CI). codex sandbox -c default_permissions="nope"stops with "default_permissions refers to undefined profilenope". With the key misspelled it stops with "config defines[permissions]profiles but does not setdefault_permissions". Both print no canary content, so the probe refuses them.codex sandboxtakes the same-ckeys but has no--ignore-user-config: the probe reads the developer'sconfig.toml, whilecodex execdoes not. The-ckeys override the same keys in that file. The probe proves that this Codex binary applies these keys to a sandboxed command; it runs throughcodex sandbox, not throughcodex execitself, which cannot run a command without a model.- Each probe took well under a second.
The two limits
- The developer's global instructions are loaded.
~/.codex/AGENTS.md(or$CODEX_HOME/AGENTS.md) appeared in the prompt input with every flag above, and the model quoted its first sentence. No configuration key removed it (instructions,user_instructions,agents_md.enabled,include_agents_md,features.agents_mdwere tried). Only a differentCODEX_HOMEleaves it out, and that moves the login: a copy ofauth.jsonwould refresh its token on its own and can leave the developer's real login with a used refresh token. The driver keeps the developer'sCODEX_HOME. In one real run the model looked forCLAUDE.mdandAGENT.mdfiles in the snapshot because the global file told it to. - The event stream does not show every command. Every current model in the catalog (
codex debug models) hastool_mode: code_mode_onlyexcept gpt-5.5: the shell is a nested tool inside a code tool. In one run on 2026-10-03, two shell commands ran (their output came back in the answer) and nocommand_executionevent appeared in the stream. In the runs on 2026-10-04 every command did appear. Nothing guarantees it, so the driver saystraced: false.
What traced: false changes in the run (packages/review/src/review-change.ts, packages/review/src/conversation.ts, packages/core/src/completion.ts):
- No read in the stream counts as coverage. A changed range counts only when its diff is in the brief or the run sent it in a correction round. A range still not sent after two rounds makes the review incomplete ("not given to the reviewer"). A changed line over 2,000 characters is given in numbered parts; a line too large for the byte budget keeps the review incomplete.
- The commands and searches the stream shows are kept in
trace.jsonwithinside: nulland their input underdetail. They never pass or fail a review: there is no "read outside the snapshot" alarm and no "tool it was not given" check. The sandbox is the boundary. - The completion record holds
trace_complete: false, and emptyfiles_readandfiles_not_read. The report prints "Files the reviewer opened: not recorded by Codex" and "Reads outside the snapshot: not recorded by Codex".
Read confinement held in every test: the permission profile is a real boundary, stronger than -s read-only. The code tool's own JavaScript runtime has no file or network access (require, import("node:fs") and fetch were all undefined or refused).
Detecting it
codex --versionprintscodex-cli 0.160.0.codex login statusprints "Logged in using ChatGPT" and exits 0 when logged in. With an emptyCODEX_HOMEit prints "Not logged in" and exits 1. The driver takes exit 0 as logged in.- A Codex session sets these variables for the commands it runs (seen in a
codex execrun, 2026-10-04):CODEX_THREAD_IDandCODEX_SESSION_ID(the thread id),CODEX_VERSIONandCODEX_CI=1. Under a sandbox it also setsCODEX_SANDBOX=seatbelt, andCODEX_SANDBOX_NETWORK_DISABLED=1when network is off.hostAgenttakesCODEX_THREAD_IDas "running inside Codex". The interactive Codex was not run for this; the binary holds the same name.
Where it works
- Started from a Bash tool inside a running Claude Code session, with the allowlist environment: it works.
- Started from inside a Codex sandbox (
codex sandbox -- codex exec ...), with network off, and again withworkspace-writeand network on:codex execexits 1 at once with "Error: failed to initialize in-process app-server client: Operation not permitted (os error 1)" and prints no event.codex --versionandcodex login statusstill work there. Sodetectreports Codex as unavailable wheneverCODEX_SANDBOXis set, andreviewprints "Full review unavailable" with thereview --agentfallback instead of starting a run that cannot answer.
Usage
Each turn.completed event carries usage. The driver adds input_tokens and output_tokens over all runs of a review and counts one turn per run. A ChatGPT login has no price per run, so the report shows no cost. The test runs on 2026-10-03 used 51,000 to 92,000 input tokens (most of them cached) and 600 to 950 output tokens per run, in 45 seconds or less. The web search run on 2026-10-04 used 52,911 in and 207 out. A real review of a six-line change with one planted SQL injection took 31 seconds, one run, 50,384 tokens in and 641 out, and reported the injection (2026-10-04).
What would remove the limits
A switch that leaves out $CODEX_HOME/AGENTS.md without moving the login, and an event for every command the code tool runs. Re-run the checks above on each new Codex version before raising the tested version.
Cursor
Not enabled. cursor-agent 2025.09.18-7ae6800 was on this Mac and not logged in. Its help shows -p ("Has access to all tools, including write and bash"), --output-format stream-json, --model, --force and --resume: no option to limit its tools, no read-only sandbox, no switch to skip the repository's rules or the developer's settings. None of the checks above can pass with those options, so no invocation was built. Cursor users get the full review through Claude Code or Codex when one is installed.
From docs/internal-reviewer-drivers.md at 68ef6b8.