Skip to content
Chimera readability score 56 out of 100, Graduate reading level.

The full audit came back: 30 of the 95 relative links pointed at files that did not exist. ./market.md

instead of market-analysis.md

, ./monetization.md

instead of business-model.md

, some files were even made up entirely! Skimming that PR as a human, I would have approved it, since every one of them looked plausible. Luckily, my automated reviewer caught it because it resolved every link instead of judging whether the names “vibed” right.

To be fair, Claude does not have any bad intent behind this. A language model simply generates the most plausible continuation, and sometimes the most plausible continuation is fiction delivered with complete confidence. From the reading side, though, that distinction brings little comfort. Confident and wrong reads exactly like confident and right.

This post is about the system I have in place to help me review Claude’s mistakes and hallucinations: a Codex agent running in GitHub Actions that reviews every pull request. It walks you through my setup, why cross-provider review beats both self-review and a human-reads-everything review, how to enforce memory and accountability in the review loop, the cost and reliability learnings I gathered along the way, and the actual code I use to make this work.

Agent throughput broke the human review loop

Hallucinations by themselves are a manageable problem, since a very careful review or functional assessment would catch them. However, what broke over the past year is the volume that needs reviewing. A single engineer now runs multiple agent sessions in parallel, and each of them can produce a thousand-line diff in the time it takes to refill your coffee. Producing code has become the fast and easy part; reviewing it honestly is what eats the day. That makes the human reviewer the bottleneck of the whole delivery pipeline, and the bottleneck is always the next thing you should automate.

That bottleneck invites two familiar failure modes. Either you approve whatever the agents produce without truly reading it, and the broken links, phantom parameters, and subtly wrong edge cases ship to production. Or you still insist on reading every line yourself, and your fast and smart AI agents work even less than you do. The first is negligent, the second wasteful, and most teams oscillate between both depending on the deadline.

The conclusion I keep landing on: our review process has to scale with the speed we’re producing code. The human element stays, but the human reviewer’s focus should go to the parts that truly matter. The grind of checking whether links resolve, whether the new parameter exists, whether there’s a bug or vulnerability, and whether the code changes match what was initially requested, all of that has to run automatically at the same cadence as the coding agents, even before a person ever spends a second reviewing the code.

Insight: hallucinations are a constant, throughput is what changed. Any process that requires a human to read every line caps your agents at human reading speed, so the very first review pass has to be automated.

Pick a reviewer that does not share the author’s blind spots

The obvious first idea to prevent these mistakes is to have the agent review its own work… but it is also the weakest one. A hallucination that made it into the diff is, by definition, plausible to the model that produced it. Asking the same weights to find mistakes in the code, especially within the same already-biased coding session, usually reproduces the exact blind spots that let the mistakes through.

This intuition has research behind it. Panickssery et al. showed at NeurIPS 2024 that LLM evaluators recognize and favor their own generations: the better a model is at identifying its own output, the more generously it scores it. Follow-up work ties this self-preference bias to familiarity: AI judges score text higher when it reads as predictable to them, and nothing reads as more predictable to a model than the text it just wrote itself.

Different LLM providers train on different data, with different recipes and different feedback loops. No provider manages to eliminate mistakes, but the kind of mistakes a model makes traces back to how it was trained, and that differs per provider. That decorrelation is exactly what you want in your review agent. In the year that I’ve used this setup, OpenAI’s Codex has consistently flagged issues in Claude-written PRs that Claude’s own review passed without blinking, from invented filenames to entire code blocks that had nothing to do with the initial request, or that introduced outright security violations.

One conclusion you cannot take from this is that Codex is the better engineer and you should hand it the keyboard instead of Claude. Codex misfires fairly regularly, too! Just in a different direction than Claude does. The power sits in working multimodel: combining providers within the same workflow, where the models fill each other’s gaps.

Another incorrect conclusion is that Codex’s reviews are perfect and can be accepted blindly. Hallucinations happen in reviews too, so treat the review as advisory. Challenging the author (be it Claude or yourself) puts the focus on potential blind spots, which is the perfect signal to zoom in and reflect. It remains the author’s job to own the final quality, and sometimes that means writing back to Codex that its review was wrong. More on this mechanism later in the post.

Insight: an author’s bugs are by definition plausible to the author, and research confirms LLMs are positively biased toward their own output. Review value comes from decorrelation, by using a model from a different provider that fails differently, and different is what catches what you miss.

The pipeline: one LLM call wrapped in ordinary engineering

Conceptually, an LLM reviewer can be written in a single line: “here is the code diff, please review it”. Both OpenAI and Anthropic ship hosted shortcuts for exactly that: OpenAI has a GitHub review integration that auto-reviews PRs, and Anthropic has claude-code-action. They are fine starting points, but I prefer to run my own action instead, because I want to own the prompt, the review lifecycle, the merge gating, and the model pin. All these aspects matter when your day-to-day becomes managing and reviewing agents.

So what am I using then, exactly? Two files that live in the repository they review:

.github/

├── workflows/

│ └── pr.yml # orchestration: triggers, context, retries, posting

└── actions/

└── codex-pr-review/

└── action.yml # the reviewer: pinned Codex call + review prompt

The runtime flow is short. Every time a PR opens or receives a push, pr.yml

checks out the code, collects the PR’s full conversation history, and hands both to the composite action. The action runs Codex over the diff and returns its findings as a single message. The workflow then posts that message as a single, continuously updated comment in the PR conversation, and publishes a commit status that stays red while findings remain unresolved: the go/no-go signal sitting next to the rest of CI.

At the center of it all sits one deceptively simple step, the only line that actually touches an LLM:

  • uses: openai/codex-action@v1

with:

openai-api-key: ${{ inputs.openai-api-key }}

prompt: |

the review contract: what to review, how to track findings,

and how to report back (unpacked in the next section)

Everything else exists to run that single step reliably, and to make sure its feedback actually lands on the PR. Here is the complete workflow:

name: PR Reviewer

on:

pull_request:

branches: [dev] # all work rides feature branches: feature -> dev -> main

types: [opened, synchronize, reopened]

a new push cancels the in-flight (expensive) review of the previous commit

concurrency:

group: pr-reviewer-${{ github.event.pull_request.number }}

cancel-in-progress: true

jobs:

codex:

if: github.event.pull_request.user.login != 'dependabot[bot]'

runs-on: ubuntu-latest

timeout-minutes: 15 # normal review takes a few minutes, so 15m is more than enough

permissions: # the token can read code and post feedback, but can never push or merge

contents: read

issues: write

pull-requests: write

statuses: write

steps:

the /head ref updates synchronously on every push;

the auto-generated merge ref can race the checkout

  • uses: actions/checkout@v7

with:

ref: refs/pull/${{ github.event.pull_request.number }}/head

make sure both diff endpoints exist locally, so Codex can resolve base...head

  • name: Pre-fetch base and head refs for the PR

run: |

git fetch --no-tags origin \

${{ github.event.pull_request.base.ref }} \

+refs/pull/${{ github.event.pull_request.number }}/head

feedback lives on three surfaces: issue comments, reviews, and inline comments

  • name: Fetch PR conversation

id: conversation

uses: actions/github-script@v9

with:

result-encoding: string

script: |

const prNumber = context.payload.pull_request.number;

const opts = { owner: context.repo.owner, repo: context.repo.repo };

const [comments, reviews, inline] = await Promise.all([

github.paginate(github.rest.issues.listComments,

{ ...opts, issue_number: prNumber }),

github.paginate(github.rest.pulls.listReviews,

{ ...opts, pull_number: prNumber }),

github.paginate(github.rest.pulls.listReviewComments,

{ ...opts, pull_number: prNumber }),

]);

const all = [

...comments.map(c => ({

date: c.created_at,

text: `@${c.user.login}: ${c.body}`,

})),

...reviews.filter(r => r.body).map(r => ({

date: r.submitted_at,

text: `@${r.user.login} (review ${r.state}): ${r.body}`,

})),

...inline.map(c => ({

date: c.created_at,

text: `@${c.user.login} (on ${c.path}): ${c.body}`,

})),

];

all.sort((a, b) => new Date(a.date) - new Date(b.date));

return all.map(e => e.text).join('\n---\n');

secrets do not travel with the yml, communicate loudly on missing keys

  • name: Fail fast on missing OPENAI_API_KEY

env:

OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

run: |

if [ -z "${OPENAI_API_KEY}" ]; then

echo "::error::OPENAI_API_KEY secret is not set for this repository."

exit 1

fi

retry wrappers cannot wrap `uses:` steps, so retry manually: transient

failures (a 401 from a fresh runner token, a hiccup reaching OpenAI)

should not turn the whole run red

  • name: Run Codex (attempt 1)

id: codex_try1

uses: ./.github/actions/codex-pr-review

continue-on-error: true

with:

openai-api-key: ${{ secrets.OPENAI_API_KEY }}

conversation: ${{ steps.conversation.outputs.result }}

give transient failures a moment to clear before retrying

  • name: Wait before Codex retry

if: steps.codex_try1.outcome == 'failure'

run: sleep 20

  • name: Run Codex (attempt 2)

id: codex_try2

if: steps.codex_try1.outcome == 'failure'

uses: ./.github/actions/codex-pr-review

with:

openai-api-key: ${{ secrets.OPENAI_API_KEY }}

conversation: ${{ steps.conversation.outputs.result }}

  • name: Post the review and publish a status

uses: actions/github-script@v9

env:

REVIEW: >-

${{ (steps.codex_try1.outcome == 'success' &&

steps.codex_try1.outputs['final-message']) ||

steps.codex_try2.outputs['final-message'] }}

with:

script: |

const MARKER = '';

const opts = { owner: context.repo.owner, repo: context.repo.repo };

const prNumber = context.payload.pull_request.number;

const raw = process.env.REVIEW;

if (!raw) return;

const body = `${MARKER}\n${raw}`;

// one canonical comment, edited in place on every push

const comments = await github.paginate(

github.rest.issues.listComments, { ...opts, issue_number: prNumber });

const existing = comments.find(c =>

c.user?.login === 'github-actions[bot]' && c.body?.includes(MARKER));

const posted = existing

? await github.rest.issues.updateComment(

{ ...opts, comment_id: existing.id, body })

: await github.rest.issues.createComment(

{ ...opts, issue_number: prNumber, body });

// go/no-go: a commit status that stays red while findings are open

// (a missing trailer reads as "review posted", never as zero findings)

const m = raw.match(/`, where N counts

the unresolved rows and M how many of those are severity High.

Always include it, even when N is 0: CI parses it into the

status check.

Pull request title and body:


${{ github.event.pull_request.title }}

${{ github.event.pull_request.body }}

Conversation history:


${{ inputs.conversation }}

Three design choices carry the weight here.

  • The review is stateful. The Feedback Summary table tracks every finding ever raised on the PR, resolved or not, across pushes. Combined with the edit-in-place comment from the workflow, a PR carries exactly one review artifact that reflects the entire lifecycle, instead of five stale comments that each reflect an update in the review process. Anyone opening the PR, human or agent, sees the full history at a glance.
  • Declines are honored. Because Codex reads the whole conversation, the author can reject a finding in writing, and the rejection sticks across review cycles. This is the mechanism that makes an imperfect reviewer workable: false positives cost one written reply instead of an endless nag loop, and the objection stays on record for the next reader.
  • The verdict is machine-readable. The trailing

line gets parsed by the workflow into aCodex review

commit status. The result is two independent signals on every PR: the workflow check tells you the review ran, and the status tells you whether anything is still open. Red status means unresolved findings; green means everything is fixed or explicitly declined. I deliberately keep the status non-blocking, since it only functions to inform. The decision to merge or not remains open, and remains human.

Note how this contract asks for more than typo hunting. Codex reviews the diff against the PR’s stated intent, ranks every finding by severity, and attaches a concrete proposed fix, the kind of review that regularly surfaces architecture-level issues no linter would ever produce.

Insight: structure beats raw model quality in a reviewer. A lifecycle table, a decline rule, and a machine-readable verdict turn free-form LLM opinions into a merge gate with a mechanism for written pushback that makes false positives cheap.

Close the loop: the PR turns green before you even look

The reviewer is only half the story when you look at the bigger engineering picture. The other half is teaching the author agent to respond to it, so the review round-trip does not become your job (remember: always try to remove the bottleneck). In my repos I use three Claude Code skills to support me, written as plain markdown instruction files under .claude/skills/

:

/pr-open

creates the PR: analyzes the branch’s commits, runs validation locally, writes an honest title and body, and targets the right base. The PR body matters more than it seems, since the reviewer reads it as the statement of intent it reviews against. I have it spell out the full feature request and reference the Linear ticket it implements, so both the reviewer and any human arriving later get the complete picture without leaving the PR./pr-iterate

handles one review round: sweeps all three feedback surfaces, then triages every item into a fix, a decline with reasoning, or a defer. Fixes are implemented and validated locally; declines become PR comments explaining why. The reviewer is advisory, and “good enough to merge” is a valid verdict once the PR meets its stated scope./pr-babysit

is the watchdog around it all: watch the CI, fix red checks, run an iterate round when new feedback lands, and loop until every check is green and every finding is fixed or declined. Its terminal state is “merge-ready”, and it stops to report rather than ping-pong commits when the same check fails twice on the same root cause.

One ordering rule that you and your agents should always keep in mind: reply first, push after. The review kicks off on every push, and it works with the context that exists at that exact moment. Anything not yet written down (a decline, a fix summary, a comment of your own) does not exist for that review round, and the reviewer will happily re-raise every item it cannot see a response to.

The end state is a tidy division of labor. Claude writes and defends, Codex challenges and tracks, the loop runs until the PR is green with a clean feedback table. My queue contains only PRs that are already merge-ready, each carrying a review trail I can audit in one scroll: what was found, what was fixed, and what was declined and why.

Insight: automate both sides of the review. When the author agent must fix or contest every finding in writing before it can push, the human queue only ever contains merge-ready PRs with an auditable review trail.

What a round-trip looks like in practice

To make the mechanism concrete, here is one exchange from a backend PR of mine. The PR fixed a capacity grid that wrote more cells than a hardcoded batch cap allowed. The first review round came back with two findings:

Feedback Summary

| Issue | Severity | Summary | Resolved |

| ------------------------------- | -------- | ---------------------------------------- | -------- |

| Truncation beyond the batch cap | High | Cells past the cap are silently dropped. | ❌ |

| Cap duplicates a platform limit | Medium | Move the cap into app config. | ❌ |

The Codex review

status turned red. Claude implemented the first finding for real, then declined the second one in a PR comment (real comment, anonymized):

Addressed the High finding (hard-coded row cap in the data layer, causing avoidable 502s).

_fetch_capacity_grid

now adapts instead of failing: on a short page (which, because the grid is dense at exactly days rows/person, unambiguously means the deployed cap truncated the batch) it halvesbatch_size

and retries the same slice, shrinking until pages fit. A short page only becomes a loud 502 oncebatch_size == 1 [...]

On the secondary suggestion (move the cap to config): intentionally not done. The real cap lives in the database’s own hosted API settings, not an app env var. Mirroring it into app config would add a second source of truth that could silently drift from the database. Adaptive batching removes the need to know the exact value at all, which is the stronger fix. Existing capacity-grid DB test still passes; lint and type-check green.

Reply first, push after, and have Codex re-review:

Feedback Summary

| Issue | Severity | Summary | Resolved |

| ------------------------------- | -------- | -------------------------------- | -------- |

| Truncation beyond the batch cap | High | Fixed via adaptive batching. | ✅ |

| Cap duplicates a platform limit | Medium | Declined: duplicate would drift. | ✅ |

Both rows resolved, one by a fix and one by a written decline that the reviewer accepted and recorded. That second row is the entire mechanism in one line: no churn was implemented just to silence a comment, and the reasoning is now part of the PR’s history for whoever reads it next.

The learnings ledger: what it costs and where it bites

This setup did not come together in one go. Most of its details grew out of surprises and dead ends along the way, so I am writing the learnings down here, in rough order of money saved, to spare you from driving into the same walls:

  • Pin the model. The

model

input ofcodex-action

is optional, and leaving it empty means “whatever Codex currently defaults to”. I learned this on my bill when the default silently moved to a newer, pricier model (gpt-5.5

). Pins solve this problem, but need maintenance in return; currently I usegpt-5.3-codex

, which is already marked deprecated on the Codex models page at the time of writing (while remaining available on the API). I revisit the pin deliberately instead of letting the default decide for me. - Cap the effort at

medium

. Higher reasoning effort catches marginally more issues for significantly more money and time. Speed matters more than it looks: the review sits inside the agent’s iterate loop, so its latency multiplies across every round of every PR, and your own waiting time is part of the price. Medium has been the issues-per-dollar and issues-per-minute sweet spot. Unfortunately, no effort level catches everything; that is what the merge-owning human is for. - Retry the quick failures. A fresh runner token occasionally 401s, and the first connection to the API occasionally hiccups. Two attempts with a gated fallback keep those from turning the workflow red “for nothing”, which matters once agents (and your own trust) treat red as a real signal.

  • Cancel superseded reviews. The

concurrency

block means only the latest commit of a PR gets reviewed. Without it, an agent pushing three fixes in a row buys you three reviews, two of them for code that no longer exists. - Review against the base that will receive the merge. My PRs target

dev

, so the review diffs againstdev

, and I keep the feature branch synced with that base. A stale branch gets diffed against a base that has moved on, and the review then reports phantom findings for changes the PR never made. - Feature branches are the prerequisite. All of this hangs off

pull_request

events. Direct pushes todev

ormain

bypass the reviewer, the status, and the audit trail entirely. The one-branch-per-change flow is what makes every change reviewable in the first place. - Assume PR text is untrusted input. Everything the reviewer reads (the PR body, the conversation, the code itself) is a prompt-injection surface, and the agent executes on your runner with your API key. The least-privilege token caps the blast radius on the GitHub side, and

codex-action

ships sandboxing and safety-strategy knobs for the runner side. Also know that GitHub does not expose secrets to workflows triggered from forked PRs, so this pattern assumes same-repo branches from trusted contributors. Review the trust model before copying it into an open-source repo.

You are the editor-in-chief now

The point of this pipeline was never to remove the human from the loop, its intent is to uplevel you. I stopped being the first reader of every diff and became the person who reads reviews, adjudicates the occasional standoff between two models, and owns the standards written into the review prompt. When Claude and Codex agree, the PR is probably fine. When they disagree, that disagreement is a precision-guided pointer at exactly the code that deserves my attention. Where Claude sometimes acts sloppy, Codex tends to over-engineer, holding that balance in your own hands is a great place to be.

Reviewing the reviews (so meta) is a better job than proofreading agent output at agent speed, and it degrades gracefully. On a lazy day I merge green PRs on the strength of the trail, on a careful day I read the feedback table and spot-check the declines. Either way, nothing reaches the merge button on one model’s confident say-so.

Key insights from this post

  • Hallucinations are a constant, throughput is what changed. Any process that requires a human to read every line caps your agents at human reading speed, so the very first review pass has to be automated.
  • An author’s bugs are by definition plausible to the author, and research confirms LLMs are positively biased toward their own output. Review value comes from decorrelation, by using a model from a different provider that fails differently, and different is what catches what you miss.
  • The model call is the easy part. Reviewer reliability comes from ordinary CI engineering around it.
  • Structure beats raw model quality in a reviewer. A lifecycle table, a decline rule, and a machine-readable verdict turn free-form LLM opinions into a merge gate with a mechanism for written pushback that makes false positives cheap.
  • Automate both sides of the review. When the author agent must fix or contest every finding in writing before it can push, the human queue only ever contains merge-ready PRs with an auditable review trail.

Final insight: work multimodel. Your agents will sometimes hand you confident fiction, and models from different providers fill each other’s gaps, so you don’t have to. A second-provider reviewer with memory and a contract, sitting between your agents and your main branch is a great place to start, but it is not the only one. Whenever one model’s output is about to matter, a second opinion from a different lab is the cheapest insurance you can buy.

Follow me on LinkedIn for bite-sized AI insights, Towards Data Science for early access to new posts, or Medium for the full archive.

Facts Only

* Thirty of 95 relative links pointed to non-existent files.
* Files were named incorrectly (e.g., `./market.md` instead of `market-analysis.md`).
* Some files were entirely made up.
* Hallucinations occur when an LLM generates plausible but incorrect continuations with confidence.
* The bottleneck in the code delivery pipeline is the human review process, which cannot scale with the speed of AI code production.
* Automated review agents are prone to producing flawed output if not properly structured.
* A system involving a Codex agent running in GitHub Actions was developed to review pull requests.
* The automated review workflow involves fetching conversation history, running the review, and posting results as comments on the PR.
* Findings from the review include checking link resolution, parameter existence, bugs, vulnerabilities, and requested changes.
* A specific example showed an agent fixing a discrepancy via adaptive batching while declining another suggestion based on a concern about data drift.

Executive Summary

The system described involves setting up a Codex agent within GitHub Actions to review pull requests, designed to catch hallucinations and errors that are missed during automated code generation. The core problem addressed is the bottleneck created by human review when dealing with high-volume AI-generated code, where engineers must manually check for functional correctness, link integrity, and adherence to intent across numerous parallel agent outputs.
The solution leverages cross-provider review, specifically using a different LLM (like Codex) than the one generating the code (Claude), to create decorrelation in the review process. This approach stems from research suggesting that LLMs exhibit self-preference bias toward their own output; thus, using a different model helps surface errors that are plausible to the author but missed by the primary agent.
The implemented pipeline orchestrates this by having an action check out the code, aggregate the conversation history, feed it to the review agent, and publish findings as structured comments directly into the pull request. The ultimate goal is to automate the high-friction aspects of review—checking for missing files, incorrect parameters, and subtle bugs—so that human reviewers focus only on architectural decisions rather than mechanical verification.

Full Take

The narrative pivots on the idea that scale demands automation not just of creation, but of verification. The fundamental pattern observed is the failure mode where speed outpaces human attention: fast AI production forces a review bottleneck that risks shipping flawed work if left to manual inspection. This dynamic suggests that cognitive load should be shifted from tedious checking to high-leverage judgment.
The insight regarding LLM self-preference bias and decorrelation taps into a deeper structural reality of multimodal AI systems: different models, trained on different data recipes, reflect distinct error profiles. The mechanism proposed—using a separate provider like Codex to challenge Claude's output—is an attempt to weaponize this diversity for fault detection, transforming the review from a passive check into an active adversarial process against plausible falsehoods.
The final automation loop, involving stateful feedback tracking and machine-readable verdicts (e.g., Go/No-Go status), suggests that true resilience is achieved when the agent loop itself becomes auditable. The final state moves beyond simple code correctness to establishing a traceable history of agreement, disagreement, and resolution. This implies that agency in the AI-augmented workflow resides not just in the generation layer but in the supervisory meta-layer—the agent responsible for managing accountability across divergent outputs.

Sentinel — Human

Confidence

This text reads as a highly experienced engineer synthesizing practical system design, LLM evaluation research, and operational bottlenecks, indicating strong human authorship focused on applied methodology.

Signals Detected
low severity: Sentence length variance is erratic; exhibits distinct shifts between descriptive narrative and highly technical procedural explanation.
low severity: Strong, cohesive argument built around a practical system (agent review pipeline), demonstrating deep personal experience and idiosyncratic emphasis.
low severity: The flow seamlessly shifts between anecdotal evidence, cited research (Panickssery et al.), technical implementation details (GitHub Actions YAML), and philosophical conclusions.
low severity: Specific, self-detailed implementation steps (e.g., `.github/workflows/pr.yml`, specific API usage) suggest grounded, lived experience rather than pure LLM generation.
Human Indicators
Use of highly specific, proprietary-feeling architectural details (GitHub Actions structure, specific action calls, secret handling nuances) indicative of direct implementation and debugging.
The development of a nuanced argument that synthesizes academic concepts (LLM bias) with practical engineering friction (throughput bottleneck) demonstrates a unique analytical path.
Idiosyncratic voice and emphasis on 'dead ends' and personal learnings ('The grind of checking whether links resolve... has to run automatically') which suggests subjective experience rather than objective summarization.
Don’t Let Claude Grade Its Own Homework — Arc Codex