← Back to Blog
Claude CodeCodexCode review

How to make Codex review Claude Code's commits: done means, house rules and round rules

by Iliya OblakovPublished on October 21, 20269 min read3 views
How to make Codex review Claude Code's commits: done means, house rules and round rules

In my last post I showed the basic loop: Claude Code writes in its own worktree, Codex reviews the commit read-only, the tests run on that same commit, and a script refuses the merge unless both pieces of evidence exist.

That loop has a weak spot. A reviewer that doesn't know what "done" means reviews against its own taste. It flags style, misses the one requirement that mattered, and finds something new every round. The loop never ends, or it ends because you got tired.

This post fixes that with three things: a definition of done the reviewer checks line by line, house rules it checks the same way, and round rules so the loop stops. I've been building this since the first Codex and Claude CLIs, and these three go into every review on my own projects.

A review without a finish line is an opinion.

The setup in one paragraph

Claude Code writes. It commits each change on a branch and stops. Codex reviews that commit in a read-only sandbox: it can read files and run read-only commands like git diff, but it can't write anything. The prompt tells it never to hand back a patch, so every fix goes back to Claude Code as a new commit, and the new commit gets a new review. The verdict is one line, Verdict: ACCEPT or Verdict: CHANGES REQUIRED, saved in a file named after the commit it reviewed.

If you want that part ready to run, the free gate is at github.com/IliyaOblakov/two-engines-gate (MIT). Everything below sits on top of it, or on top of your own scripts.

Step 1: write the brief before the code

Most review noise starts before any code exists. If the only instruction Claude Code got was "add CSV export", the reviewer has nothing to check the result against except its own opinion.

So every task starts with a short brief. Mine has four parts:

# Brief: CSV export for the orders page

## Done means
1. A "Download CSV" button on /orders downloads the orders currently
   shown, with the current filters applied.
2. Columns, in order: id, created_at (ISO 8601, UTC), customer_email,
   total (two decimals, no currency sign), status.
3. A customer email that contains a comma or a quote is escaped, so
   the file opens in a spreadsheet with the right columns.
4. A new test covers point 3 and fails on the code before this change.

## Must stay exactly as it is
- The JSON response of GET /api/orders.
- Existing filters and their URL parameters.

## How it is checked
- Point 1: the route and the button in the diff.
- Points 2 and 3: the new test.
- Point 4: the test run once against the base commit, output pasted
  in the commit body.

## Out of scope
- Exports for other pages. Scheduled exports.

Every line under "Done means" has to be something a reviewer can confirm from the diff or from a test. "Export works well" isn't a line. "A customer email with a comma is escaped" is.

Point 4 is worth stealing on its own. The engine that writes the code often writes the tests too, and a test that already passes on the old code proves nothing about the change. Ask the writer to run each new test once against the base commit and show that it failed there.

Where the brief lives matters. Commit it with the change, or keep it under .ai/. The free gate refuses to review a checkout with uncommitted changes or untracked files anywhere else, and it skips .ai/ for exactly this kind of working file. If you add .ai/ to your .gitignore, commit that change too, or the edited .gitignore itself gets the review refused.

Step 2: house rules the reviewer can grep for

The brief covers this task. House rules cover every task: how errors are handled, where config lives, what never goes into a log. The things you'd otherwise repeat in every review.

Write them like the brief, as things a reviewer can find in a diff:

## House rules
- No new dependency without a line in the commit body saying why.
- Config comes from environment variables read in config.ts only.
  No process.env anywhere else.
- Never log a full email address, token or request body.
- User-facing strings go through t(), not hard-coded text.

Put them in the files both engines already read: CLAUDE.md for Claude Code, AGENTS.md for Codex. The writer follows them while writing, and the reviewer checks against the same text. Keep the AGENTS.md section short: Codex stops adding instruction files once their combined size reaches project_doc_max_bytes, 32 KiB by default.

Write the rule once. Check it every time.

Step 3: make the reviewer answer every line

Now change what you ask for. Instead of "review this diff", ask for an answer per line:

The brief for this change is in .ai/brief.md.

For each line under "Done means" and each house rule, answer one of:
- met: give the file:line in the diff that shows it
- not met: give the file:line and what is missing
- not checkable: say what evidence would be needed

Treat the commit message and the writer's summary as claims to verify,
not as evidence. If a section is empty, say "not given" instead of
guessing what it should have said.

Findings outside the brief and the house rules need a concrete failure:
the input or steps, and what goes wrong. Style preferences are not
findings.

Keep the verdict rule: exactly one line, Verdict: ACCEPT or
Verdict: CHANGES REQUIRED. ACCEPT only when every line above is met.

With the free gate, add this to review-prompt.md, which exists to be edited with your project rules. With your own scripts, append it to whatever prompt you send Codex.

The reviewer now checks a list instead of free-styling. And a "not checkable" answer is a finding about your brief: you wrote a requirement nobody can verify.

Step 4: cap the rounds

Without round rules, a reviewer at high effort can find a new nit every round, and the change never lands. So each round gets one job:

  • Round 1 lists every finding. Nothing held back for later.
  • Round 2 confirms the fixes and may only add what the fixes broke.
  • Round 3 and later: only blockers and security findings hold the merge. A blocker needs a failure you can reproduce. Everything else is a nit for a follow-ups file, and the reviewer can accept.
  • At the cap: if the last allowed round still says CHANGES REQUIRED, stop sending fixes. The verdict stays CHANGES REQUIRED, so the merge stays blocked, and a person decides: change the approach, split the change, or allow one more round on purpose.

Add the rules to the prompt:

Round rules. Round 1: list every finding. Round 2: confirm the fixes and
add only what the fixes broke. Round 3 and later: only blockers and
security findings may hold the merge; mark everything else as a nit for
the follow-ups file.

The reviewer also needs to know which round it's in. Count rounds in a small file per change:

#!/usr/bin/env bash
# rounds.sh <change-name>: count review rounds for one change; exit 3 at the cap
name="${1:?usage: rounds.sh <change-name>}"; max="${MAX_ROUNDS:-3}"
case "$max" in ''|*[!0-9]*) echo "MAX_ROUNDS must be a whole number" >&2; exit 1 ;; esac
root="$(git rev-parse --show-toplevel)" && cd "$root" || exit 1
f=".ai/reviews/$name.rounds"
mkdir -p .ai/reviews
n=0; [ -f "$f" ] && n=$(wc -l < "$f")
n=$((n + 1))
if [ "$n" -gt "$max" ]; then
  echo "last round reached for $name; a person decides" >&2
  exit 3
fi
echo "$n $(git rev-parse --short=12 HEAD) $(date -u +%FT%TZ)" >> "$f"
echo "round $n of $max"

Then feed the count into the prompt. With the free gate, a short wrapper does it. Save it as review-round.sh in the root of your repository, in the same folder as rounds.sh, and run it from that root:

#!/usr/bin/env bash
# review-round.sh <change-name>: one counted review round with the free gate
round="$(bash rounds.sh "$1")" || exit
{ cat path/to/two-engines-gate/review-prompt.md; printf '\nThis is %s.\n' "$round"; } > .ai/prompt.md
REVIEW_PROMPT=.ai/prompt.md path/to/two-engines-gate/bin/review.sh

Commit both scripts, or list them in .gitignore and commit that change, so the gate does not refuse them. The .rounds file and .ai/prompt.md they write sit under .ai/, which the gate skips.

A review that fails without any verdict still uses up a round. Delete the last line of the .rounds file before you rerun it.

The cap never turns into an ACCEPT. It only stops the loop and hands the decision to you.

Step 5: when Claude Code and Codex disagree

Sometimes the writer pushes back on a finding, and sometimes it's right. Don't let the two engines argue it out over five rounds. One rule: a finding stands if it comes with a reproducible failure, and goes to follow-ups if it doesn't. Anything the brief doesn't settle, like scope or anything a user would see, is your call. That's one short question to you, not another round.

Evidence wins. Opinions wait.

What this doesn't do

None of this makes code correct. A reviewer can mark a line "met" and be wrong, and tests only check what they test. What you get is a review that checks the things you said mattered, and a loop that ends.

Try it

The free gate has the read-only review, the verdict named after the commit, the test pass record, and a merge check that refuses without both, plus an offline demo that needs neither CLI. It runs in both directions: BUILDER=codex makes Claude Code the reviewer. Add the "Done means" section, the house rules and the round rules to its review-prompt.md, put rounds.sh and the wrapper in your repository root, and you have everything in this post.

If you want the full workflow I use, the Two Engines Kit is a paid kit I made. It adds one rulebook both engines load, hooks and settings for both engines, a GitHub merge script that merges exactly the commit that was checked, and an installer that previews every change and can be undone byte for byte. $29 for the first 30 sales, then $39: rayastats.com/en/two-engines-kit.

Rate this article

Be the first to rate this article • 3 views

More from the blog