Jiufeng
Loop in Practice: Claude Code + Codex as a Closed Loop

Loop in Practice: Claude Code + Codex as a Closed Loop

From Maker-Checker separation and the Task Packet to machine gates and approval evidence — a working AI coding setup you can actually run. Includes two real defects (rate limiting built backwards, a CAS conflict misread) and a minimal work-order template you can copy.

Vibe Coding

Part two of the series. Part one covered why you need a Loop. This one covers how to build one: what Claude Code and Codex each do, how two AIs collaborate without validating each other, how to avoid endless review and document bloat, and what a minimum viable Loop actually looks like. There's a copy-paste work-order template at the end.

1. A failure you've probably seen

Start with a scenario you've likely lived through.

You ask Claude Code to add login rate limiting. It's efficient about it: writes the middleware, adds config, adds tests. Runs them — all green. It reports:

Login rate limiting is done. The middleware is wired in; tests cover threshold triggering and per-IP isolation. All passing.

Sounds airtight. What do you do? Probably merge.

But this implementation has three problems, and not one of them turns a test red:

One: it built something other than what you asked for. You wanted a count of consecutive failed logins. It built per-IP limiting on all requests. Successful logins burn quota too. And the bucket is shared with critical endpoints like payments — meaning a user who logs in normally a few times can eat into their own payment quota.

Two: it widened the scope on the way. The work order explicitly listed registration and two password-reset endpoints as "out of scope, pending a decision." The actual diff had already changed all three. Not malice — just "those look like they should be limited too."

Three: the tests don't exercise the path that ships. The test calls the rate-limit middleware directly, never going through real route registration. It proves "this function's threshold works under miniredis." It cannot prove this round's routing change.

I didn't make these up — they're three MAJOR findings from a single review round.

And if you ask Claude "is this right?", it'll say yes. Not because it's lying — because it just used that same reasoning chain to produce the implementation. Ask it to self-review and it walks the chain again.

Self-review is a tautology, not a check.

2. Why Maker-Checker

Banks have used this term for decades: the person who executes and the person who verifies must be two different people. Not because the executor is untrustworthy — because nobody has distance from what they just did.

Ported to AI coding, there are four roles:

RoleIsOwnsNever owns
Claude CodeMakerimplement to the work order, run tests, report, fix findingsself-approval, widening scope, committing or deploying
CodexCheckerindependent review of the real diff, machine-readable verdictreplacing the compiler, deciding product risk
YouDecidergoal priority, architectural semantics, risk acceptance, release authorityrelaying messages between two windows
Hook / GuardMachine boundarypath scope, dangerous operations, evidence integrityjudging business semantics

One point deserves to stand alone:

The Checker must read the real diff, never the Maker's account of it.

If you paste Claude's summary into Codex and have it review the summary — the seam is gone. It's reviewing "what Claude thinks it did," not "what Claude actually did." In that rate-limiting case, Claude's summary — "tests cover threshold triggering and per-IP isolation, all passing" — was true in every word. It just didn't mention that the tests bypassed the real router.

How did Codex find it? It put the middleware implementation, the controller's success path, the default quota config and the route registration side by side, then pointed out: the success path never clears the bucket, and that bucket is shared with payment endpoints.

That's not "smarter." That's reading different inputs.

There's also a common objection worth killing:

Can't I just self-review in a fresh session? Or with a different model?

No. As long as what it reads is the previous step's output plus the previous step's explanation, it's on the same chain. The seam isn't "a different model" — it's different input. The Checker's input has to be the real diff, not a narrative.

3. Minimum viable Loop: five components

You don't need to build a whole system on day one. The minimum has five pieces:

1. Authoritative docs — requirements, architecture, plan. They answer why and what it should look like. If you have none today, even one sentence of architectural invariant works — but there must be something that doesn't belong to this round and can't be edited by this round, or the Maker will "optimize" the goal itself.

2. The Task Packet — this round's single goal, allowed paths, acceptance criteria, explicit prohibitions. It's the one main-line pointer the executing session has. Template at the end.

3. Claude Code executing — implement to the packet, run tests, report.

4. Guard + tests — the machine boundary. The Guard answers exactly one question: did the real diff leave the allowed paths? It doesn't judge whether the code is good. The tests answer a different one: what's the exit code?

5. Codex review + evidence pack — independent review of the real diff, machine-readable verdict; on approval, freeze the HEAD, diff, test output and verdict from that moment.

That's it. No board, no timesheets, no status reports.

4. How one task runs

confirm the goal (human)
   ↓
write this round's work order: one goal / allowed paths / acceptance / prohibitions
   ↓
Claude Code implements + runs tests
   ↓
Guard: did the real diff go out of bounds?   ──yes──► send back
   ↓ no
Codex reviews independently (reads the real diff)
   ↓
  ├── APPROVE_PHASE ──► freeze evidence ──► human decides commit / next phase
  ├── REQUEST_CHANGES ─► fix only BLOCKER/MAJOR ──► regression ──► re-review
  ├── ESCALATE_USER ──► stop; hand facts + recommendation to the human
  └── STOP ──► stop immediately, protect the tree (security / production / irreversible)

Each verdict maps to a specific action — and each one has a "don't":

VerdictDoDon't
APPROVE_PHASEstop editing, check the evidence pack, hand offkeep fixing NITs, refactor while you're there
REQUEST_CHANGESfix only BLOCKER/MAJOR and what was askedwiden scope, rewrite the approach
ESCALATE_USERreport facts, impact, recommendation, open decisionslet the executor pick architectural semantics
STOPstop, protect the tree and the environmentroute around the gate and try again

"No NIT fixes after APPROVE" sounds petty but matters: the approved state is a freeze point. Touch the code after approval and the evidence pack no longer matches — and an evidence pack that doesn't match is no evidence pack.

5. Two real defects

Both satisfy the same condition: the implementation looks right and the tests pass.

Case one: the rate limiter was built backwards

The one from the opening. Codex's finding (desensitized):

MAJOR — the middleware implements per-IP limiting on all requests against a shared bucket, not a count of consecutive failed logins; the success path never clears the bucket either. A successful login consumes the same default 60-per-20-minutes quota, and that bucket is shared with critical endpoints such as payments. The acceptance semantics — "does a success reset the count, are normal users unaffected" — cannot currently be met.

Why unit tests can't catch this: the test verifies "past the threshold, requests get rejected" — which it does correctly. No test asks "is the count cleared after a successful login," because that isn't a standard middleware test. That's requirement semantics.

Codex only saw it by putting the middleware, the controller's success path, the default quota and the route registration — four files — side by side.

Note the last line: Codex did not decide how to fix it. It stated that "whether to keep a pure per-IP request quota or move to failure-counting with reset-on-success is a security product decision requiring the user" — that's ESCALATE_USER, not REQUEST_CHANGES.

The Checker's job is to find and characterize, not to pick your product strategy.

Case two: a CAS conflict read as exhaustion

This one goes deeper. It came from round five on the same design:

MAJOR — a CAS conflict when claiming a candidate is wrongly treated as the candidate list being exhausted. The design closes out with NONE after a second CAS failure, and NONE is converted into a terminal error. But a CAS failure can also mean another worker already reserved/dispatched a valid attempt. The correctness contract requires insert-or-load of the existing attempt and deferred handling for a live DISPATCHING attempt. It must first re-read the active attempt / existing claim / task state and choose join, ACK/drop, defer, or continue from the latest cursor. Only after proving there is no in-flight attempt and the sequence really is exhausted may it return NONE.

In plain terms: two workers race for the same candidate, and the loser reads "someone else took it" as "there are none left" — and kills the whole task.

A single-threaded test will never see this. You have to run two workers in your head.

Together these two show where a Checker earns its keep: it isn't catching syntax or style — it's catching cross-file semantics and concurrency, exactly the layer where the compiler, the type checker and the unit tests are all silent.

If your independent review only ever finds typos and formatting, the seam isn't working — fix the review prompt.

6. Keeping the Loop from slowing you down

This is the most important section. The most common failure after adding process isn't that the process doesn't work — it's that the process crushes you.

Six hard rules:

1. One phase, one goal. Not "one phase per module" — one phase per class of problem. The "single goal" field takes exactly one thing. If it doesn't fit, split.

2. Review after a round is complete, not during. Don't review in dribs and drabs. The Checker's cost is in understanding the whole change; hand it half a change and you get half an opinion.

3. Three rounds max. More than three on the same review prompt usually means the packet, the design, or the review boundary is wrong — not that the code is one tweak away. Go fix the packet, not the code.

I've broken this myself: one architecture design took five rounds to converge. Looking back, the rule was what needed fixing — architecture design is something you converge on by getting pushed back on, and applying an implementation task's ceiling to it was wrong. So the rule now reads "three rounds for implementation; design is negotiated separately."

4. No NIT fixes after APPROVE_PHASE. The approved state is a freeze point. Want those small things? Next packet.

5. Small tasks skip the Loop. Reading code, chasing a bug, fixing copy, a low-risk single-file change — just do it, run the targeted check, done. Writing a work order for that is self-harm.

6. Side issues get recorded, not chased. You'll always find other problems mid-implementation. Four destinations — pick one:

ClassWhenAction
IMPLEMENT_NOWcan't finish the current goal without it, and it's in scopefix and test this round
ACCEPTED_RISKrisk understood, current goal still deliverable, human acceptedrecord it, don't block
LATERvaluable but not this goalnext packet
ESCALATE_USERchanges architectural semantics, scope, priority or release riskstop, hand over evidence + recommendation

Finding a side issue doesn't mean fixing it now. Leave this unwritten and the Loop becomes an infinite TODO collector by day two.

One more backstop signal: two rounds in a row that only edit the packet, the review prompt and the ledger, with no code progress — that isn't rigor, that's spinning. When you see it, lower the weight. Don't add documents.

7. Copy this: the minimum work order

The most practical part of this post. Save it as packet.md and fill it in before each round:


## The one goal this round
<exactly one thing. Doesn't fitit's two rounds>

## Architectural invariants (must not break this round)
- <one line each. This is what the Checker judges against>

## Allowed change surface
- `src/xxx/**`
- `test/xxx/**`
> Write directories as path/**, not prose. This field must parse into a machine allowlist.

## Explicitly not doing
- <next phase's work>
- <side issues found but out of scope>
- no commit / no push / no deploy unless explicitly asked this round

## Acceptance commands
- guard: <command that compares the real diff against the allowlist>
- test:  <concrete command that returns an exit code>

## Risks the independent review should focus on
- <the 1-3 places most likely to break this round>

A few notes, all learned the hard way:

"Allowed change surface" matters more than "goal." A goal is a sentence of natural language; the Maker can read it a hundred ways. An allowed surface is a set of paths a machine can compare against the real diff. Only a constraint the machine can enforce is a constraint.

"Explicitly not doing" isn't a formality. In that rate-limiting case, the scope creep happened precisely because the packet listed registration under "not doing" and included it in the goal — two fields contradicting each other, so the Maker did it.

Don't dump history into the review focus. One to three of this round's riskiest points. Give the Checker a hundred and it spreads itself thin; give it three and it digs.

Acceptance commands must produce an exit code. "Run the tests and make sure it's fine" isn't an acceptance command. go test ./... -run TestXxx is.

Conclusion

Two models isn't about having two models.

Codex isn't smarter than Claude, and Claude isn't worse than Codex. Those defects would have been caught by two Claudes, or two Codexes, as long as three things hold:

  • separation of duty: the Maker doesn't approve its own output;
  • machine gates: the critical constraints are things that return a non-zero exit code, not a sentence saying "please comply";
  • an evidence loop: what gets approved isn't "both AIs said it's fine" — it's the real diff + exit codes + an independent verdict, frozen together.

The output of a Loop isn't the word "approved."

It's a verifiable, recoverable, auditable approved state.


Next: main-line governance — why AI always drifts from the architectural goal, and how to write the "single goal" field so that it actually holds.