← All posts
· 5 min read

Reviewing code with two AI models

A single AI reviewer can miss a bug, or state a wrong finding with total confidence. Running two independent reviewers on the same diff, having each one check the other's claims, and only then merging the results catches more and turns disagreements into a signal worth reading,…

Codex and Claude review panels independently inspecting a code diff, then cross-checking their findings Codex and Claude review panels independently inspecting a code diff, then cross-checking their findings

A single AI reviewer can miss a bug, or state a wrong finding with total confidence. Running two independent reviewers on the same diff, having each one check the other's claims, and only then merging the results catches more and turns disagreements into a signal worth reading, instead of noise. Codex Claude Toolkit is a review script built around exactly that idea.

Two ways to run it

Codex Claude Toolkit opens with a mode choice: Standard, which runs a fixed, opinionated sequence with no further questions, or Customize, which asks about every step. Both branches share the same functions underneath; only the questions differ.

Either mode starts by picking a scope: all files in the repository, or only the current Git changes. For the changed-files path, git status is read with --porcelain=v1 -z instead of the normal newline-separated output, because a NUL-delimited stream survives filenames with spaces, quotes, or Unicode without breaking the parser, and a rename entry's second path is deliberately consumed and discarded so only the current path is reviewed.

Cheap checks before any model runs

Before either AI is invoked, local tools do the fast, free checks: ShellCheck lints the review script itself, SwiftLint runs against the changed Swift files (or the whole project), and SwiftFormat can rewrite files in place. SwiftLint's raw output is noisy, so it gets piped through awk to count warnings and errors per file and print one icon instead: ❌ for a file with errors, ⚠️ for warnings only, ✅ for a clean file.

SwiftFormat is opt-in, and its arrow-key prompt defaults to "No" even though every other yes/no prompt in the script defaults to "Yes" — the one step that rewrites the working tree gets a different default on purpose.

Running two models in parallel

Only after the cheap checks pass does the script spend time and tokens on model review. Codex and Claude both run read-only, against an identical prompt built from the same instructions, the same required output format, and the same git diff:

codex exec --sandbox read-only -C "$repository_path" \
  -o "$codex_output_path" - <"$review_prompt_path" &
codex_process_id=$!

claude -p --model haiku --permission-prompts none \
  <"$review_prompt_path" >"$claude_output_path" &
claude_process_id=$!

wait_for_process "$codex_process_id" "$REVIEW_TIMEOUT_SECONDS"
wait_for_process "$claude_process_id" "$REVIEW_TIMEOUT_SECONDS"

Because the prompt is identical for both, differences in their findings come from the models, not from how each one was asked.

A strict, one-line output format

The prompt doesn't just ask for a review; it asks for a specific grammar:

Return exactly one finding per line with no Markdown bullets or extra paragraphs.
Use this format:
🔴 [HIGH] path/to/file.swift:123 - finding
🟠 [MEDIUM] path/to/file.swift:45 - finding
🟡 [LOW] path/to/file.swift:8 - finding
If there are no findings, return exactly: 🟢 No issues found.

Constraining the model's output to one finding per line, with a fixed severity icon and an explicit sentinel for the empty case, is what makes the rest of the pipeline possible: later stages can grep and count findings mechanically instead of asking another model to summarize free-form prose.

Cross-checking before merging

Each model's raw findings still need a check against the actual code. Every reviewer gets a second prompt containing the original diff plus the other model's findings, and is asked to confirm or reject each one and add anything missed:

Claude has completed an independent review below.
Now verify Claude findings against the code, confirm or reject them, and add
any missing findings.
Codex independent findings:
...

A third pass then merges both cross-checked lists into one, tagging every line [BOTH], [CODEX], or [CLAUDE] depending on who raised it, and keeping the original severity icon:

🔴 [BOTH] [HIGH] Sources/Sip/Brewer.swift:123 - ignores the cancellation token
🟠 [CODEX] [MEDIUM] Sources/Sip/Timer.swift:45 - timer leaks on early return

A finding both models reach independently, after checking each other's work, is worth trusting more than either one alone.

Gating on the result, and timing out safely

The merged list decides the exit code: any [BOTH] finding, or any HIGH severity finding from either model, fails the run. A disagreement between two models isn't automatically wrong, but it's a clear signal to go read the code before shipping.

Every model call is wrapped by wait_for_process, a small polling loop: it checks once a second whether the process is still alive, and after REVIEW_TIMEOUT_SECONDS (300 by default) it sends TERM, waits a second, then KILLs if the process is still there. Plain bash wait has no built-in timeout, so this loop is what stops one slow model call from hanging the whole review indefinitely. A trap on EXIT cleans up every temporary file the run created, and a separate trap on HUP/INT/TERM makes a Ctrl-C exit with status 130 instead of leaving things half-finished.

In practice

Cheap local checks run before any model is invoked at all: ShellCheck on the script itself, SwiftLint and SwiftFormat on the changed Swift files, an optional Xcode build and test pass with a timeout.

Only after those pass does it spend the time and tokens on two independent model reviews, their cross-check, and the synthesis.

Every full run writes four timestamped reports to disk — the combined summary plus separate Codex, Claude, and comparison files — so a review from an hour ago is still there to compare against, and colors and the clear screen only show up when the script is actually attached to a terminal.

Codex Claude Toolkit is public: github.com/ruiaureliano/codex-claude-toolkit.

← All articles