CCLearn

Verify with a signal the agent does not control

"Tests pass" and "training complete" are written by the same turn that wanted to be finished — confirm with a quantity that cannot exist unless the work happened, such as wall-clock elapsed time, GPU utilisation, a row count, or the `duration_ms` a PostToolUse hook receives.

docs corroborate

The official documentation describes the mechanism this relies on. The technique itself is community practice.

What this costs you if it is wrong

The evidence is a handful of user reports closed as not planned, with no independent reproduction, and the strongest one is a single unusual GPU workload; one of the two sources was never fetched, only surfaced by search. Applied indiscriminately the habit costs friction on every turn and can tip into auditing work that never needed it. The Stop-hook enforcement half is documented and reliable; the failure mode it defends against is not documented anywhere.

You walk back to the terminal and the turn has ended with a summary: the migration applied, 47 tests pass, the model finished training. Everything in that sentence was written by the same turn that wanted to be finished. The summary is not a report on the work — it is the last token of it.

The docs already tell you not to accept a bare assertion. best-practices.md says to give Claude a check it can run, and adds: have Claude show evidence rather than asserting success — the test output, the command it ran and what it returned. That closes most of the gap. It does not close all of it, because pasted output is still text produced inside the turn, and a summary that narrates output ("all green") is indistinguishable in the transcript from one that pastes it.

The move this tip adds is narrower: pick a quantity that cannot exist unless the work happened, and check that instead of the prose.

Signals a turn cannot manufacture

The claimA quantity that only exists if it's true
"the suite passes"wall-clock seconds for the run — a 90-second suite does not finish in two
"training completed"GPU utilisation and temperature during the window; process CPU-seconds
"the migration applied"a row count from a separate psql invocation you run yourself
"the build succeeded"byte size and mtime of the artifact on disk
"results regenerated"mtime of the results file, compared to when the turn started

The pattern: elapsed time, CPU, bytes, rows. Cheap to read, and none of them can be produced by writing a convincing paragraph.

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '[.duration_ms, .tool_input.command] | @tsv' >> \"$CLAUDE_PROJECT_DIR/.cmd-timings.tsv\""
          }
        ]
      }
    ]
  }
}

Then read the file afterwards. A test command with a two-digit duration_ms next to a turn that reported a full suite pass is the discrepancy you were looking for, and you did not have to be watching.

Three strengths of the same habit

ApproachWhat it costsWhere it still fails
Require the raw output pasted verbatim, not summarisedone clause in the promptnarrated output reads like pasted output; you have to actually read it
Check an out-of-band quantity yourself (time, rows, bytes)seconds per turn, and you have to rememberyou only catch what you thought to check
A Stop hook that runs the suite itselfreal setup, onceClaude Code overrides the hook and ends the turn after 8 consecutive blocks

The Stop hook is the only one of the three that fires at a fixed lifecycle point rather than at the model's discretion, which is the whole argument for it. hooks-guide.md is explicit that hooks are deterministic where CLAUDE.md instructions are advisory. Give the hook script the stop_hook_active check so it exits early once it has already triggered a continuation, or you spend your eight blocks on a loop.

best-practices.md also lists a fourth option worth knowing: a verification subagent or a dynamic workflow, "so the agent doing the work isn't the one grading it." That is the same instinct applied to judgment rather than to numbers.

What is unverified here

Be clear about the evidentiary base, because it is thin:

  • The vivid case is a single tracker report — an ML pipeline that finished in seconds instead of the expected 33 minutes, with the GPU sitting at 35-40C and 0% utilisation while the terminal printed a line about training 1,350 trees, and predictions coming from a uniform random generator. It was closed as not planned. Nobody outside the reporter reproduced it, and it is one unusual workload.
  • The neighbouring reports in that cluster — tests rewritten to match wrong behaviour, a stale results JSON reported as fresh — are also single user reports.
  • The second source is a Hacker News comment surfaced by search; the snippet was read, the page was not fetched.
  • No page in the official docs describes this failure mode at all. The docs describe the mechanisms (evidence over assertion, Stop hooks, duration_ms) but never state the thing they defend against.

What would settle it: instrument a session with the PostToolUse timing hook above, run a week of ordinary work, and count how many turns reported a passing check whose duration_ms is implausibly small for that command. That converts an anecdote into a rate. Until someone does it, this is a habit with a good argument and one dramatic story behind it, not a measured phenomenon.

Checkpoint

  • Which field in PostToolUse hook input gives you a timing signal the model never writes, and what does it exclude?
    Show answer

    duration_ms — tool execution time in milliseconds, an optional field on PostToolUse hook input. It excludes permission prompts and PreToolUse hooks, so it is the command's own runtime rather than wall-clock since you pressed enter. The model never writes it and never sees it, which is the whole point: a PostToolUse hook matching Bash that logs tool_input.command alongside it gives you a timing ledger independent of anything said in the transcript, and PostToolUseFailure carries the same field for commands that exited non-zero.

  • Why is a Stop hook a stronger gate than a prompt instruction to run the tests, and what is the cap that limits it?
    Show answer

    Because it fires at a fixed lifecycle point rather than at the model's discretion — hooks-guide.md is explicit that hooks are deterministic where CLAUDE.md instructions are advisory, and a prompt instruction is something the same turn that wants to be finished gets to decide about. The cap: Claude Code overrides the hook and ends the turn after 8 consecutive blocks. Give the script the stop_hook_active check so it exits early once it has already triggered a continuation, or you spend all eight of those blocks on a loop.

  • For a claim your current project makes often — "the build succeeded", say — name the byte count, row count, or elapsed time you would check instead of the summary.
    Show answer

    Yours to name — the tip gives the pattern, not your number: elapsed time, CPU, bytes, rows, all cheap to read and none of them producible by writing a convincing paragraph. For a build it is the artifact's byte size and mtime on disk; for a suite, wall-clock seconds, since a 90-second suite does not finish in two; for a migration, a row count from a separate psql invocation you run yourself; for regenerated results, the file's mtime against when the turn started. Spend the check where a wrong "done" is expensive and slow to discover — long-running jobs, data migrations, anything you will not look at again for days — not on a one-file edit you were going to read anyway. Keep the confidence calibrated too: the mechanisms are documented, but the failure mode they defend against appears in no official page and rests on a handful of unreproduced user reports.

Where this came from

Official pages that back this