CCLearn

The trust-then-verify gap, and the green that isn't

verified against Claude Code 2.1.261 · 2026-09-0915 min

By the end you can

  • Name the trust-then-verify gap and the documented fix for it
  • Read a transcript for the override, the stall and the impossible verdict
  • Get a second read of the diff from a context that never saw the reasoning
  • Set /code-review effort as a confidence-versus-coverage dial, not a quality dial
  • Discount a reviewer's findings without discarding the review

You come back after an hour. The last message says the migration is done and the suite is green. Lesson 5.1 taught you to scroll up for the artefact — what was run, what it returned — and the artefact is there.

This lesson is about the cases where all of that is true and the work is still wrong. Everything so far built a check. This is about reading its outcome without being fooled by it.

The trust-then-verify gap

The docs name this among the common failure patterns. The name is well chosen: the trust happens now, the verification happens later, and the bug lives in the gap.

The trust-then-verify gap. Claude produces a plausible-looking implementation that doesn't handle edge cases. Fix: Always provide verification (tests, scripts, screenshots). If you can't verify it, don't ship it. — Best practices

Read the fix as two claims. The first — always provide verification — is Lessons 5.1 through 5.4. The second is stricter than it looks: if you can't verify it, don't ship it. Not "ship it and watch". An absent check is a reason to stop, and that includes the case where a check existed but never actually ran.

Three ways a green run is not green

A run can end with a reassuring final message while the gate you built was never satisfied. These are documented behaviours, not bugs, and each leaves a different mark in the transcript.

What happenedWhat the transcript showsWhat it means
The gate was overriddenA warning that the Stop hook blocked too many consecutive timesThe check never passed. Claude Code let the turn end anyway
The /goal loop stalledA warning, control handed back, the goal still setNothing resolved. Evaluation restarts on your next prompt
The goal was judged impossibleThe goal is cleared, a failed entry recorded with a reasonThe evaluator decided the condition can never hold

The override. Lesson 5.2 built the Stop hook and named its cap. Here is the same fact read from the other end:

Claude Code overrides a Stop hook after it blocks eight times in a row without progress. — Hooks guide

The turn ends, and it does not end silently: "Claude keeps working instead of stopping, then ends the turn with a warning that the Stop hook blocked too many consecutive times" (Hooks guide). That warning is the only thing separating this ending from a real one. The work is unfinished by your own definition, and the gate was overridden rather than satisfied.

The stall.

If Claude keeps answering the evaluator without making progress (no tool use for several turns in a row), Claude Code stops the loop, prints a warning, and returns control to you with the goal still set. — Set a completion goal

"With the goal still set" is the tell. An achieved goal is cleared and recorded as achieved. A goal still sitting there when you get back ran out of patience.

The impossible verdict. This one is the cruellest, because it does clear the goal:

Impossible: the evaluator judged that the condition can never be satisfied. — Set a completion goal

A cleared goal is what success looks like from ten feet away. The difference is one word in the transcript entry — achieved versus failed — and the reason beside it.

The habit this asks for is small: when a long run ends, read the warnings, not just the last message. Every ending above prints one except the impossible verdict, which records a failed entry in the transcript instead.

Even a goal that resolves cleanly carries a limit worth restating:

It doesn't run commands or read files independently, so write the condition as something Claude's own output can demonstrate. — Set a completion goal

"Met" means met in what Claude reported. If Claude ran the suite and the output landed in the transcript, the evaluator read a real result. If the condition was phrased so a confident sentence satisfies it, the evaluator read a sentence.

The grader must not be the writer

Every mechanism so far shares a weakness: the thing being checked and the thing checking are the same context. It read its own reasoning, it knows why the approach was chosen, and it is the least likely reader in the world to spot the assumption that made the bug. The fix is structural, stated as a property rather than a feature:

A reviewer running in a fresh subagent context sees only the diff and the criteria you give it, not the reasoning that produced the change, so it evaluates the result on its own terms. — Best practices

The same principle is visible in a shipped product. The security guidance plugin — the one that reviews Claude's own edits as it works — is unusually direct about it:

The plugin does not ask the same Claude instance that wrote the code to grade itself. … the reviewer starts from the diff, has no investment in the original approach, and is instructed only to find problems. — Security guidance

Three properties do the work, and you can arrange all three by hand: starts from the diff, no investment in the approach, instructed only to find problems. Independence is not a mood the reviewer is in. It is what you withhold from it. And it pays in proportion to how long you were away: "the longer Claude works unattended, the more an independent check matters before you count the work as done" (Best practices).

Invoking it

For correctness there is a bundled version, and no reason to write your own:

text
/code-review

For a correctness check, run the bundled /code-review skill, which reviews the current diff for bugs in a fresh subagent and returns findings to the session. — Best practices

It reviews your branch's commits ahead of its upstream plus any uncommitted changes; pass a target — a file path, a PR number, a branch, or a range like main...my-feature — to review something else.

Now read "in a fresh subagent" literally, because that phrase is carrying the whole property the section above just described:

/code-review runs as a forked subagent from v2.1.218; on earlier versions it ran inline and stacked. — Skills

The subagent it forks does not get your side of the conversation — "It won't have access to your conversation history" (Skills) — so the reviewer really does start from the diff, and the cost claim rides along with it: "The review runs as a background subagent with its own context window, so it doesn't fill your conversation". Before v2.1.218 neither held. The review ran inline, in the session that wrote the code, which is a second pass and not a second opinion.

The background half of that is not unconditional. A /code-review typed while an earlier one is still running, a -p or Agent SDK run, and CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 each run the review in the foreground.

What the command supplies for correctness, you still arrange by hand everywhere else. A review prompt typed into the session that just did the work is graded by the context that wrote it. Ask for the review in a subagent explicitly, the way the prompt below does, or run it somewhere the reasoning never arrived — a session started fresh on the diff, given your criteria and nothing else. That is Lesson 3.2's point about what a cleared context stops carrying, used deliberately instead of suffered.

The remaining choice is on the findings themselves:

At low and medium, the review reports only the findings it's most confident in, so you see fewer false positives; high through max broaden coverage and may include findings the review is less sure about. — Code review

That is a confidence-versus-coverage dial, not a quality dial. max is not "the good one" — it is the one that shows you more, including things it is unsure about. Right when a miss is expensive; wrong when you are triaging a small diff.

To check a diff against your plan rather than for bugs, write the prompt yourself and name what counts as a finding — and say subagent, because a prompt typed into the working session inherits the conversation history a forked review never receives:

text
Use a subagent to review the rate limiter diff against PLAN.md. Check that
every requirement is implemented, the listed edge cases have tests, and
nothing outside the task's scope changed. Report gaps, not style preferences.

A review is not a gate

This is the trap, and it sits one level above the gate that was overridden.

None of the layers block writes or commits. Findings reach the writing Claude as instructions, Claude addresses them in the conversation, and the review model can miss issues. — Security guidance

A review that ran and reported nothing is not the same event as a Stop hook that passed: the hook asserted a condition, the reviewer offered an opinion. Rung four sits at the top of 5.2's ladder without being a stronger gate than rung three — it answers "is this right?", not "did this finish". You still need rungs one through three for the second question.

Discounting the findings without discarding them

The other half of using a reviewer is not believing all of it.

A reviewer prompted to find gaps will usually report some, even when the work is sound, because that is what it was asked to do. — Best practices

Chasing every finding is how a sound change acquires extra abstraction layers, defensive code, and tests for cases that cannot happen. The documented mitigation is instructional, not a judgement made afterwards:

Tell the reviewer to flag only gaps that affect correctness or the stated requirements, and treat the rest as optional. — Best practices

Which is why the prompt above ends with "report gaps, not style preferences" — the scope goes in the brief, before the findings exist and before you have a stake in dismissing them.

The last distinction is the one to carry out of this level. It comes from how the bundled research workflow reports its own results:

When the verifier agents can't check a claim, such as after a rate limit or API error, the report lists that claim as unverified instead of counting it as refuted. — Workflows

Unchecked is not disproved. The overridden hook, the stalled goal, the review that ran at low effort: none of them refuted anything. They stopped checking, and stopping is not passing.

The level in one line

Give it a check it can run. Site the check where it gates the stop. Then read the ending — a run stops for many reasons and only one of them is that your check passed. If you cannot say which one it was, the work is unverified, and unverified work does not ship.

Checkpoint

  1. A /goal run you left for an hour has ended, the goal is no longer listed, and the final message describes a completed migration. Name three endings consistent with that description, and say what you would look at to tell them apart.

    Show answer

    Three endings clear a goal. It was met — the evaluator judged the condition satisfied and the goal was cleared and recorded as achieved. It was judged impossible — the evaluator decided the condition can never hold, which also clears the goal but records a failed entry with a reason. Or the turn hit an unrecoverable error — an authentication failure where Claude Code manages its own credentials, an exhausted credit balance, a context overflow auto-compaction could not clear, or an unavailable model — and Claude Code cleared the goal with a warning starting Goal cleared after an unrecoverable error.

    Tell them apart by reading the warnings and the transcript entry rather than the last message. The first two differ by one word — achieved versus failed — and the reason beside it; the third announces itself in its warning. Note what the premise rules out: the stall hands control back with the goal still set, so "no longer listed" means this was not that. And even an achieved entry is worth one more look, because "met" means met in what Claude reported — if the condition was phrased so a confident sentence satisfies it, a sentence is what the evaluator read.

  2. /code-review max on a 40-line diff returns six findings, four of them about missing abstraction. What do you do with them, and what should have been in the brief?

    Show answer

    Act on the correctness findings and treat the four abstraction ones as optional. A reviewer prompted to find gaps will usually report some even when the work is sound, because that is what it was asked to do, and chasing all of them is how a sound change acquires extra abstraction layers, defensive code and tests for cases that cannot happen. The effort level is part of the explanation: max is a confidence-versus-coverage dial, not a quality dial — low and medium report only the findings the review is most confident in, while high through max broaden coverage and may include findings it is less sure about. On a 40-line diff you are triaging, which is the documented case for the other end of the dial.

    The brief should have carried the scope: tell the reviewer to flag only gaps that affect correctness or the stated requirements and treat the rest as optional — "report gaps, not style preferences". The mitigation is instructional, not a judgement made afterwards, and the timing is the whole point: the scope goes in before the findings exist and before you have a stake in dismissing them.

  3. Your Stop hook runs the suite and it has just gone green after a long unattended run. Does that make the change correct? Which of this level's two questions does the hook answer, and which does it not?

    Show answer

    No. The hook answers "did this finish?" — rungs one to three make the run finish properly, and a passing suite says the run met the condition you defined for ending. It does not answer "is this right?", which is rung four's question and belongs to a model that did not write the code. A plausible-looking implementation that does not handle an edge case passes a suite that never covered that edge case; that is the trust-then-verify gap itself.

    Two things follow. Check that the green is the hook passing and not the hook being overridden: after eight consecutive blocks Claude Code ends the turn anyway with a warning, and that warning is the only thing separating that ending from a real one — stopping is not passing. Then get the second question answered by a context that never saw the reasoning, /code-review being the bundled way to do it: a forked subagent that starts from the diff without your conversation history. Do not mistake that for a stronger gate, though — none of the review layers block writes or commits, findings reach the writing Claude as instructions, and the review model can miss issues.

Sources