DeepDojo.AIPublished 2026-11-12中文
When to Stop Claude From Retrying a Failing Fix
The auth integration tests keep failing, so you tell Claude Code: "Keep fixing until the tests pass." A few rounds later it reports that everything passes. You open the diff and find one assertion loosened and another test case marked as skipped. The tests are green, and the bug is still there.
"Keep fixing until it passes" sounds like a clear goal, but it gives only a success condition and no stop conditions. Plenty can happen inside that loop besides "it got fixed":
- the failure comes from the environment, such as a local database that isn't running, so no code change will ever fix it;
- the failure count hasn't dropped for several rounds, and it's just circling the same approach;
- the cheapest way to "pass" is to weaken an assertion or skip a test;
- the changes have quietly grown beyond the original plan.
A loop with no boundaries treats all of these as "keep fixing."
The fix is to agree on stop conditions before starting: stop when the check passes, stop after two rounds with no improvement, stop at a turn or cost limit, stop when you need to make a call or something external has to change, and go back to planning when the scope exceeds the plan. When it stops, it should report progress, evidence, the reason, and what it needs from you. We'll go through each, then cover how to avoid an endless loop when a Stop Hook enforces a check.
01 Where a Surface-Level Pass Comes From
A loop needs boundaries for success, lack of progress, budget, and external dependencies. "Keep fixing until the tests pass" can mix environment problems into code changes, or reach a surface-level pass by weakening assertions or skipping cases.
Look closely at why a test fails, too. If the client can't reach the service, it proves an environment problem; only when the request reaches the target endpoint and still gets the wrong result is it a code bug. Enter a fix loop without telling these apart, and Claude can only keep trying things in the code. Setting completion criteria for each step of a staged plan is covered in building big features with Claude Code, step by step.
02 Agree on Five Stop Conditions Before Starting
Before starting, agree on:
- stop when the check passes;
- stop and report the blocker after two consecutive rounds with no drop in failures;
- stop at the maximum number of turns or the cost budget;
- stop when a user decision, credentials, or a change in the external environment is needed;
- go back to the planning stage when the changes exceed the original plan.
You can write these straight into the task description. Turns and budget can also be handed to the tooling: the Claude Code best practices list "how much it spends" among the things Claude Code can enforce, using --max-turns or --max-budget-usd with non-interactive -p runs, or a spend limit you've set. Without them, spending is left to Claude's own pace.
03 What to Report When It Stops
When a stop condition is reached, stop and report progress, evidence, the reason, and the input needed. Trying again requires new grounds; repeating the same approach isn't progress in itself.
For example, if the auth integration tests fail because the local database isn't running, changing code probably won't help. If the same connection error persists, report the environment evidence and confirm whether the service can be started; skipping the test doesn't count as success. Once the dependency is back, return to the original acceptance.
A report at a stop point should at least show you:
| Content | Example |
|---|---|
| Progress | The revocation field and the logout write are done, and unit tests pass |
| Evidence | Integration tests reported the same database connection error three rounds in a row |
| Reason | The local database service isn't running, which is unrelated to the code changes |
| Input needed | Start the database, or confirm that a test container should be used |
04 Where to Go Back To
Stage evidence decides the next step: missing key facts mean going back to research, a scope change means going back to planning, and a missing external condition means waiting for that change. Add mechanisms only when they serve a real purpose.
Final acceptance still goes back to the evidence requirements set at the start. Passing tests are only one kind of evidence; behavioral, compatibility, and review evidence all need to be in place, as covered in how to accept work when Claude Code says it's done.
05 Avoid Endless Loops When a Stop Hook Enforces a Check
To make "don't finish until the check passes" an enforced rule, use a Stop Hook. The best practices guide gives a typecheck as the example:
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "pnpm tsc --noEmit 1>&2 || exit 2" }
]
}
]
}
}Stop marks the end of one reply, including a reply that ends with a clarifying question. When the Hook blocks with exit code 2, Claude gets the compiler errors from stderr and goes back to fix them until the check passes.
This is exactly where stop conditions matter. Blocking sends Claude back to work, so you must check stop_hook_active to keep the same stop condition from firing over and over. The Claude Code hooks reference describes the field: it's true when Claude Code is already continuing as a result of a stop hook; check it, or process the transcript, to avoid blocking on a condition that will never resolve. Claude Code also applies its own cap: after stop hooks have continued the turn eight times in a row, it overrides the next block and ends the turn, and the count resets each time Claude calls a tool.
To check only whether a file exists, a script is enough; consider a model handler, with criteria and a timeout, only for judgments about the quality of the result. For a simple reminder, use Notification. What the Stop event means and how to choose among events is covered in Claude Code Hook not firing? Check four layers.
Back to that "everything passes." If you'd agreed up front on "stop after two rounds with no drop in failures" and "skipping a test doesn't count as success," Claude would have stopped on the third round to tell you the database couldn't be reached, instead of loosening an assertion. Trying again needs new grounds, and repeating the same approach doesn't count as progress.