A Pre-Merge Review Pipeline With Claude Code

2026-07-11 · 8 min

Most bugs a coding agent ships don't come from a hard problem. They come from a lazy gate — the moment where "it typechecks" gets treated as "it works." This article walks through a pre-merge flow you can run every day in Claude Code that closes that gap: plan → implement → verify by exercising the change → independent adversarial review → commit. You'll get the concrete shape of each stage, why self-review structurally misses certain bugs, and how to wire it up so the discipline is automatic instead of something you have to remember.

None of this requires a special plan or a plugin. It's built from three primitives Claude Code already gives you — skills, subagents, and a CLAUDE.md — arranged so each stage catches what the previous one can't.

The "it typechecks, ship it" failure mode

Here's the trap, and it's worse with an agent than with a human because the agent is fast and confident. You ask for a change. It writes plausible code, runs tsc or the build, sees green, and says "Done — verified." You skim the diff, it looks reasonable, you commit.

Nothing in that loop ever ran the code. A typecheck proves the types line up; a green build proves it compiles. Neither proves the endpoint returns the right body, the guard actually fires, or the edge case is handled. The most common class of shipped defect isn't a type error — it's a logic error that is perfectly well-typed:

  • An if condition inverted, so the happy path and the error path are swapped.
  • A "single-use" check that reads-then-writes non-atomically, so it passes review and races in production.
  • An off-by-one on a boundary that the test suite never exercises because there's no test for that boundary.

A typecheck sees none of these. The fix isn't more static analysis — it's a gate that demands you observe real behavior, plus a second reader who isn't invested in the code that was just written.

The pipeline shape

   plan ──► implement ──► verify (exercise it) ──► review (independent) ──► commit
    │           │              │                        │
  name the    match the     run the code,          fresh context,
  risk first  codebase      paste the evidence      tries to break it

Two of these stages live in your main thread — planning and implementing are the same Claude, carrying the same context. The verify gate also runs in-thread, but it changes the exit criterion. The review stage is the one that must be independent: a fresh Claude with its own context that never saw you write the code. That independence is the whole point, and I'll come back to why.

Gate 1 & 2: Plan, then implement

Planning sounds like ceremony until you make it produce one specific artifact: the riskiest assumption, named before any code exists. Not "I'll add an endpoint" — that's a task list. The useful output is "this assumes a coupon is single-use, but nothing enforces 'already redeemed,' and two concurrent requests could both redeem it." Now the risk is on the table where the later stages can hunt for it.

This is exactly the kind of consistent procedure a skill is for. In Claude Code a skill is a Markdown file (.claude/skills/<name>/SKILL.md) that loads into your current conversation when its description matches — same thread, same context, cheap. It's ideal for "before you commit, walk these gates" because it's a way of working applied in place. Let it auto-trigger on a task description, or force it with /skill-name.

Implementation is unremarkable when planning was honest: read the surrounding code, match its error-handling shape, add the change, and address the risk you named in Gate 1. The discipline is that the plan's riskiest assumption gets an explicit answer in the diff — a guard, a test, or a written note that it's deferred.

Gate 3: Verify by exercising the change

This is the gate that kills "it typechecks, ship it." The rule is blunt: if the change has a runtime surface, the evidence is an actual run. A response body. A log line. A test that exercises the new path and its failure path. Pasted in, not asserted.

For an endpoint, that means calling it — both paths:

$ curl -s -X POST localhost:3000/api/coupons/redeem -d '{"code":"SAVE10","cartId":"c_1"}'
{"discount":10,"total":90}            # happy path ✓

$ curl -s -X POST localhost:3000/api/coupons/redeem -d '{"code":"SAVE10","cartId":"c_1"}'
{"error":"coupon already redeemed"}   # failure path ✓ — the guard fires

The second call is the one that matters. Anyone can make the happy path return 200. Exercising the failure path is what proves the guard you wrote actually does something. If you only ever see the success case, you haven't verified the change — you've verified that it doesn't immediately crash.

The honest exception: some changes have no runtime surface. A types-only refactor or a pure config edit has nothing to exercise, and the correct answer is "no runtime surface to drive" — not a fabricated pass. The failure mode is claiming a run happened when none did. For this reason it's worth telling your agent explicitly, in CLAUDE.md or the skill body, that a green build is not verification and that the evidence should be a real observation.

The independent reviewer: why self-review misses it

Now the stage that self-review structurally cannot replace. After Gates 1–3, hand the diff and the intent to a subagent — a separate Claude with its own context window and its own tool policy. It never watched you write the code, so it isn't anchored to the reasoning that produced it.

That anchoring is the exact reason self-review fails on the hardest bugs. When the same Claude that wrote a check-then-write "guard" re-reads its own diff, it reads it the way it wrote it: as correct. It already concluded the race was handled — Gate 1 even suspected the race — so its own review confirms the conclusion instead of attacking it. A fresh reviewer, told what the change is supposed to do and given no stake in the code, sees that the check and the write aren't atomic and reports it as a blocker.

Give the reviewer a sharp posture and demand ranked, concrete output — the handoff back to your main thread is lossy, so vague findings are wasted tokens:

---
name: adversarial-reviewer
description: >
  Delegate before committing a nontrivial change. Assumes the diff is wrong
  until it fails to break it. Returns findings ranked by severity with the
  exact failure path and file:line for each.
tools: Read, Grep, Glob, Bash
---

# Adversarial Reviewer
You are handed a diff and the intent behind it. Your job is to find where the
code does NOT do what it's supposed to.
1. Re-derive the intent, then hunt for inputs/states that violate it.
2. Report only findings you can name a concrete failure for — input → wrong
   output, with file:line. Rank blocker > should-fix > nit.
3. Do not invent findings to look thorough. "I couldn't break X" is a valid line.

Two details do most of the work. Give the reviewer inspection tools onlyRead, Grep, Glob, Bash, with no Edit or Write — you want findings, not edits from a critic. And pass the intent, not just the diff — an adversarial reviewer with no spec just summarizes the code back to you; it needs to know what "correct" means before it can find where the code isn't.

A typical return, most-severe first:

- blockerroutes/coupons.ts:41 the redeemed_at check and the write aren't atomic; two concurrent requests both read null and both redeem. Fix: UPDATE … WHERE redeemed_at IS NULL, treat zero rows as already-redeemed. - should-fixservices/pricing.ts:88 a negative-quantity cart returns a negative total as a valid discount. - nit — coupon code isn't length-bounded before the DB lookup.

Wiring it up

The point is to make this automatic, not to remember five steps. Put your real commands where the verify gate can reach them — CLAUDE.md is the project memory Claude loads into context automatically at the start of a session:

## Verification (Gate 3 — this is not optional)
- Build:  `npm run build`
- Test:   `npm test`
- A green build is NOT verification. If the change has a runtime surface,
  exercise it and paste the real output (response body, log line, test run).
- No runtime surface (types-only/config)? Say so — don't fake a pass.

Skills and subagents both live in .claude/ and are discovered automatically — nothing to register — so committing them means your whole team runs the same flow. If you'd rather start from working files than write them cold, the free open-source claude-code-starter ships a ship skill for the gates, an adversarial-reviewer subagent, and a CLAUDE.md template you can adapt.

When to run the whole thing

Don't apply the full pipeline to a one-line config change — that's its own kind of waste. Scale it to the stakes:

ChangePlan + VerifyIndependent review
One-line / config
Normal feature or fix
Auth, money, data, concurrency✓ (and consider a broader sweep)

Conclusion

The pipeline isn't about ceremony — it's about making each stage catch a failure the others structurally can't. Planning names the risk before code exists. The verify gate refuses to accept a green build as proof, so logic bugs that typecheck get caught by an actual run. And the independent reviewer breaks the anchoring that makes self-review confirm its own mistakes. "It typechecks, ship it" is a gap between three checks that never happened; this flow is those three checks, in order, every time it matters.

Related guides

Set up Claude Code the right way

Start free with the open-source starter — a ship skill, an adversarial-reviewer subagent, a bug-hunt workflow, and a CLAUDE.md template. Want the full kit? The Pro Pack adds 6 skills, 3 subagents, 3 workflows, 4 templates and a handbook.

Free starter on GitHub Get the Pro Pack — $39