Stop Conditions: Making ‘Done’ Mean Something

TL;DR

  • A loop is only as trustworthy as its stop condition. “Done” has to be a condition a different model can check (tests pass, lint clean, schema validates), never the worker’s own claim.
  • Climb the four rungs: a bare while ! npm test loop with a cap, then a Stop hook (exit 2, 8 blocks auto-override), then /goal (a separate model grades the condition each turn), then scheduled goal runs.
  • Make the checker a separate agent, write conditions that are verifiable, falsifiable, and cheap to evaluate, and cap every loop with the three hard stops. The hands-on run below is real.

A loop that grades its own work will tell you it is done before it is. That is the whole problem this part solves. In Part 2 you named the verifier block and shipped a minimal loop; here you make its stop condition trustworthy. A verifiable stop condition is the difference between a loop you can leave running and one you have to babysit. This builds directly on single-session checking from the self-verification loop; now we generalize it across runs.

A loop is a recursive goal: you define a purpose and a verifiable stop condition, and the system iterates agents against it until the condition holds — without you prompting each turn.

Prerequisites: a working harness, a minimal loop you can run, Claude Code v2.1.139+ for /goal, and a repo with a test command that exits non-zero on failure.

Why does self-grading fail?

A maker grading its own output is structurally optimistic, so it declares victory early. Anthropic’s own writeup on building effective agents lists this failure mode by name: premature victory and fake-done features, which happen precisely because an unverifiable “done” has nothing to push back on. The fix is one sentence: “done” must be checkable by a different model than the one that did the work.

Look at the two shapes side by side. A self-report reads “looks good to me, the feature works.” An external check reads “the test in test/auth exits 0.” The first is a feeling the maker reports about its own homework. The second is an artifact anyone, or any other agent, can run and confirm. Only the second survives the maker being wrong.

This splits stop conditions into two honest philosophies, and the checker section returns to them: a condition graded by another model, or a machine-checkable artifact like a green CI run or a problem count that reaches zero. Best practice combines both.

Key insight: A loop’s stop condition is a claim about reality. If the agent that wrote the code is the only thing asserting the claim, you have a self-report, not a verification.

What are the four rungs of a stop condition?

Stop conditions come in four rungs, each adding rigor the rung below it lacks. Start at the bottom and climb only as far as the job needs.

RungMechanismWhat it adds over the rung below
1Bare while ! npm test; do claude -p ...; done + iteration capa machine-checkable exit code as the gate
2Stop hook (exit 2 or {decision:"block"})blocks completion until the check passes
3/goal (separate model grades each turn)a grader that is not the maker
4Scheduled goal runs (Routines)the condition is re-checked unattended

Rung 1 is the whole loop in one line: while ! npm test; do claude -p "fix the failing test"; done. The test command is the condition, and its exit code is the gate. Even here, cap it (capping comes last); an uncapped rung-1 loop is how you wake up to a drained token budget.

Rung 2 moves the gate into a Stop hook. The hook returns exit 2 (or JSON {decision:"block"}) to refuse completion while the condition is unmet, and Claude Code auto-overrides after 8 consecutive blocks so a stuck loop cannot wedge forever. The cap is built into the mechanism.

Rung 3 is where the checker stops being the maker. /goal (Claude Code v2.1.139+) has a separate, smaller model, Haiku, grade the stop condition each turn rather than trusting the worker’s claim. This is the rung most loops should reach for: you state the condition in plain language, and a different model decides each turn whether it holds.

Rung 4 fires that same /goal condition on a cadence with Routines, so “done” is re-checked while you are away. The cadence machinery is Part 6; here it is enough to know the top rung is a scheduled grader.

Key insight: The rungs are an escalation of who checks. Rung 1 checks with an exit code, rung 3 checks with a second model, and the gap between them is exactly the maker’s optimism.

How do you write a verifiable stop condition?

A good stop condition passes three tests: it is verifiable (something can confirm it), falsifiable (it can fail), and cheap to evaluate (you can run it every turn). Phrase “done” so it survives all three, or the loop will not.

Bad conditionWhy it fails the testVerifiable rewrite
”the feature is done”not falsifiable, no check”all tests in test/auth pass AND lint clean"
"code looks good”not verifiable by a machinepasses: true for every story in prd.json"
"the bug is fixed”no boundary”the new regression test exits 0 and no other test breaks”

The canonical anti-pattern is “run day and night until the feature is done.” It reads like a goal and behaves like a trap, because nothing in it can be checked, so the loop either stops on a hunch or never stops. snarktank/ralph avoids this by stopping only when every PRD story reads passes: true, a machine-checkable artifact rather than a vibe.

The move to internalize: turn every vague “done” into a command that exits zero or non-zero, or a count that has to reach a target. If you cannot write that command, you do not yet have a stop condition.

Key insight: “Run until the feature is done” is not a stop condition, it is a wish. A stop condition is a command with an exit code.

Why should the checker be a separate agent?

A checker with different instructions, and optionally a different model, removes the maker’s optimism for the same reason you do not merge your own PR unreviewed. The maker is invested in being finished; the checker only cares whether the condition holds. Splitting them is the single highest-leverage move in loop design, and it is the loop-internal cousin of running code review on agent output.

In practice the checker enforces one of the two philosophies introduced at the start, ideally both: it runs a machine-checkable artifact (a test, a lint, a schema validation) and, on a rung-3 loop, lets a separate model grade the condition. continuous-claude ships this shape with an optional reviewer pass and a --completion-signal flag; ralph encodes it as the passes: true gate a fresh agent has to satisfy each lap.

There is a real design axis here worth knowing before you wire it. The official ralph-wiggum plugin runs the checker in the same session as the maker, and the community has flagged that this deviates from fresh-context Ralph, where each lap (maker and checker alike) starts clean. Same-session is cheaper; fresh-context is harder to fool. Pick deliberately.

A checker is just an agent file. The reusable shape:

---
name: stop-condition-checker
description: Grades whether the loop's stop condition holds. Not the maker.
model: haiku
---
You verify, you do not fix. Run the project's check command and report only:
- PASS if `bash tests/run.sh` exits 0 AND `bash scripts/lint.sh` exits 0
- FAIL otherwise, with the first failing line
Never edit code. Never report PASS on the maker's say-so; run the command.

Key insight: The checker’s value comes entirely from not being the maker. Different instructions, a cheaper model, and a clean context each turn are what make its PASS mean something.

Hands-on: looping an agent until a real test passes

To make this concrete I ran a real loop against a real defect in this content-agent plugin repo. The bug: scripts/check-draft-seo.sh checks whether the primary keyword is front-loaded in the title, but it compares every keyword word against only the title’s first three words. Any keyword longer than three words is wrongly flagged, even when it leads the title verbatim.

First the failing test (the red). The fixture uses the 6-word keyword “how to build an agent loop”, which leads its title and also sits in the meta description and an H2, so the check should pass. It did not:

⚠ keyword_placement: keyword "how to build an agent loop": not in title first 3 words
FAIL: multi-word keyword that leads the title was wrongly flagged.
exit=1

The stop condition was a good one by the good-vs-bad test from the previous section, machine-checkable and falsifiable: bash tests/test-check-draft-seo-multiword-keyword.sh exits 0 AND bash scripts/test-rubric.sh still exits 0. That second clause is the “no other test breaks” guard.

A note on rung 3 versus what I actually ran. /goal is the natural fit here, and per the docs it would have Haiku grade the condition each turn. But /goal is an interactive in-session grader, and I wanted a captured, reproducible run, so I used the rung-1 bare-loop equivalent: a fresh-context claude -p as the maker each turn, and the bash test exit code as the checker. That is an honest maker/checker split, graded by a machine-checkable artifact rather than by Haiku. If you want the Haiku-graded version, that is /goal itself.

The loop, capped at 8 turns, passed on turn 1. The fresh-context maker read the failing test, located the fixed three-word window, and windowed the title to max(3, len(kw_words)) instead, so a longer keyword that leads the title is recognized while short keywords keep their old behavior. The functional change was small (7 insertions, 2 deletions, most of it an explanatory comment):

all_title_words = re.findall(r"[A-Za-z...]+", title.lower())
kw_words = re.findall(r"[A-Za-z...]+", kw_lower)
title_words = all_title_words[:max(3, len(kw_words))]

📊 Result proof. One fresh-context maker turn. Pass on turn 1 (cap of 8 never reached). After the turn: the regression test exits 0, scripts/test-rubric.sh still exits 0, and a control draft whose keyword is genuinely not front-loaded still warns correctly, so the fix is real, not a check that now always passes. Token and dollar cost were not instrumented for this run, so I will not quote a number I do not have.

That is the entire lesson, and it is short on purpose: the value is the verifiable stop condition, not a long log. A bad condition would have needed me to read the diff and decide; the test decided.

Key insight: The loop stopped because a command exited 0, not because the agent felt finished. That is the only kind of “done” you can leave a loop alone with.

How do you cap a loop so it can’t run forever?

Every loop gets a cap, because a stop condition can be wrong or unreachable. The converged practice across serious 2026 writeups is three hard stops: a max iteration count, no-progress (stall) detection, and a token or dollar ceiling. No mature system runs uncapped.

Each maps to a concrete control. The iteration count is claude -p --max-turns N for a headless run, plus the Stop hook’s 8-block auto-override from rung 2. Stall detection is continuous-claude’s --stall-threshold, which kills a loop that stops making progress. The budget ceiling is --max-cost and --max-duration on the same tool. Wire at least one from each category.

For lived precedent: our own content-agent review loop caps at 3. A careful, in-house verification loop still chooses a hard number over “until it is perfect,” because the alternative is a loop that polishes forever. Your stop-condition checklist, the reusable artifact from this post, is three lines:

  • Verifiable: something can confirm it (a command, a count, a second model).
  • Falsifiable: it can fail, with a clear failing signal.
  • Cheap: you can evaluate it every single turn.

Key insight: The stop condition says when to stop on success. The cap says when to stop anyway. A loop you trust has both.

FAQ

What is a verifiable stop condition?

A verifiable stop condition is one a different model or a machine check can confirm, such as tests passing, lint clean, a schema validating, or passes: true in a file, rather than the worker’s own claim that it is done. It is verifiable, falsifiable, and cheap enough to evaluate every turn.

What is the difference between /goal and a Stop hook?

A Stop hook blocks completion with exit 2 until a check passes, and Claude Code auto-overrides after 8 consecutive blocks. /goal (v2.1.139+) instead has a separate model, Haiku, grade the stop condition each turn. The hook is a gate you write; /goal is a grader you delegate to.

How do you stop an agent loop from running forever?

Cap it three ways: a max iteration count (claude -p --max-turns N, plus the 8-block Stop-hook override), no-progress detection (--stall-threshold), and a token or dollar ceiling (--max-cost, --max-duration). Those are the converged three hard stops.

Why shouldn’t an agent grade its own work?

Because a maker grading its own output is structurally optimistic and declares “done” early, the documented premature-victory failure mode. Use a checker with different instructions, optionally a different model, and ideally a clean context each turn.

  • Part 4: Memory Outside the Context Window (loop-memory-state-files, ships next in the Loop Engineering series). A verified “done” is worthless if the loop forgets it between runs, so Part 4 builds the state-file spine that makes a stop condition stick.
  • The self-verification loop, the single-session prequel this part generalizes into a cross-run stop condition.
  • Anatomy of a loop, Part 2, where the verifier block was named and deferred to here.