What to Do When Claude Code Gets Stuck in a Loop
When Claude Code gets stuck in a loop overnight, make it try something different each time and stop with a note after 4 tries. Wrong API keys, blocked files and an empty budget are never retried, and each try gets its own slice of the budget.
>This covers the classify step and the four-rung ladder. Claude Code Loops goes deeper on the state file rung two reloads from, the stall rule that triggers a climb, and the verifier whose reason rung one feeds back.

Hello builders,
The best morning after an overnight Claude Code run is one where the loop hit a wall, recovered by itself, and shipped, or stopped with a note saying exactly why it gave up. The usual morning is worse: the gate refused at 2:14am, the loop tried the same thing again, and by the time you woke up it had done it eleven times and billed you for all eleven. If you have a Claude Code agent stuck in a loop like that, the fix is a classify step plus a four-rung recovery ladder the loop climbs on a failure count, with nobody awake to decide.
Why does the plain retry do so badly? When our first attempt fails, that attempt is now in the context, so attempt two is conditioned on a transcript where the wrong approach is the most recent, most detailed thing present. What we have really handed it is a worked example of the thing that failed, plus a request to keep going. I will be straight about the strength of that claim, because I have not measured it and I haven’t found anyone who has. Anthropic’s own guidance on long-running work points the same way: record failed approaches, or successive sessions re-attempt the same dead ends.
So every rung we build has to change a condition. A retry that changes nothing is a re-roll of dice already weighted against you.
The pattern has a public name too. Code Intel Log’s production hardening write-up puts agent loops behind a circuit breaker with four states:
- CLOSED: normal operation, no limits
- WATCHING: failure rate elevated (>10%), monitoring closely
- DEGRADED: high failure rate (>30%), reduced max iterations
- OPEN: critical failure rate (>50%), agent disabled
Its degradation moves are model downgrade, tool set reduction, max iterations reduction and human handoff. Our ladder borrows that shape and swaps failure rates for a per-step count.

Classify before you climb
Some failures are the loop being told no, and retrying them is paying to hear it again. A SCOPE REFUSAL from your hook is a rule you wrote on purpose, and four creative attempts to get around it is the exact behaviour the boundary exists to stop. budget_exhausted means you already said there’s no money to climb with. A 401 or ECONNREFUSED stays broken however many times you retry, and the agent will get inventive against a wrong API key. If you’ve ever found a fabricated mock where a real integration should be, this is usually how it got there. Never retry these four.
classify() { # reads the terminal reason, returns an action
case "$1" in
*SCOPE\ REFUSAL*) echo halt ;; # a rule, not an obstacle
budget_exhausted) echo halt ;; # no money to climb with
*ENOENT*|*ECONNREFUSED*|*401\ Unauthorized*|*403\ Forbidden*) echo halt ;;
*) echo climb ;;
esac
}
Four rungs on a count
Everything else climbs, and we make every trigger a count the loop can evaluate by itself. At 3am there is nobody to decide. We keep the policy as data in loop-contract/recovery.tsv:
# rung trigger action budget_share
1 failures_this_step == 1 retry_with_reason 0.15
2 failures_this_step == 2 retry_fresh_context 0.25
3 failures_this_step == 3 retry_downgraded 0.35
4 failures_this_step >= 4 handoff 0.00
Why give each rung its own slice of the budget? Because a loop that spends freely on rungs one and two arrives at rung three broke. The last column is each rung’s share of the run’s budget. Set LOOP_BUDGET_USD once and rungs one to three get 15%, 25% and 35% of it, so the rung most likely to work still has money when the loop reaches it. Rung four gets 0.00, because handing off should not cost anything. The dispatcher reads that column, so the file really is the policy. It also reads two things your loop should already keep: loop-contract/*.md, your project facts and house rules, and .loop/state.md, a short file rewritten every iteration that lists what was tried and rejected, each with the error that rejected it. Put the dispatcher in .loop/lib.sh next to your wrapper:
share() { # $1 = rung; returns that rung's slice of LOOP_BUDGET_USD from the policy file
awk -v t="${LOOP_BUDGET_USD:-5.00}" -v r="$1" '$1==r{printf "%.2f", t*$NF}' loop-contract/recovery.tsv
}
step_failed() { mkdir -p .loop/failures; echo x >> ".loop/failures/$1"; wc -l < ".loop/failures/$1" | tr -d ' '; }
recover() {
local step="$1" reason="$2" n; n=$(step_failed "$step")
case "$n" in
1) claude -p --max-turns 10 --max-budget-usd "$(share 1)" "$(cat loop-contract/*.md)
The previous attempt at '$step' was refused. The verifier said: $reason
Address that specific objection. Do not restate the previous approach." ;;
2) claude -p --max-turns 10 --max-budget-usd "$(share 2)" "$(cat loop-contract/*.md .loop/state.md)
Task: $step. Approaches already tried and rejected are in the state file above. Do not repeat any of them." ;;
3) CLAUDE_CODE_SUBAGENT_MODEL=haiku claude -p --max-turns 6 --max-budget-usd "$(share 3)" "$(cat loop-contract/*.md .loop/state.md)
Task: $step. Two approaches have failed. Reduce this task to the smallest change that could possibly work, state it in one sentence, and implement only that." ;;
*) printf 'ESCALATED\t%s\t%s\n' "$step" "$reason" >> .loop/terminal; exit 4 ;;
esac
}
Call it wherever your loop handles a refusal, with $step set to the name of the step that failed, right after the verifier writes its verdict, so a halt becomes a named hand-off at once:
reason=$(jq -r '.reason' .loop/verdict.json)
if [ "$(classify "$reason")" = halt ]; then
printf 'ESCALATED\t%s\t%s\n' "$step" "$reason" >> .loop/terminal
else
recover "$step" "$reason"
fi
Rung one feeds the verifier’s objection back as an instruction. Rung two throws the conversation away and reloads from the state file, which breaks the conditioning problem and is the rung most of us skip. On rung three, read the prompt before the model line, because two failures usually mean the task was bigger than the agent’s grasp of it. Rung four writes ESCALATED to .loop/terminal and stops.
One trap on that third rung. ANTHROPIC_DEFAULT_HAIKU_MODEL looks like the obvious knob, but it is read everywhere the small fast model is used, including /goal’s evaluator, so setting it mid-recovery quietly swaps the judge of your stop condition. CLAUDE_CODE_SUBAGENT_MODEL changes the worker and leaves the judge alone.
We test it the way we’d test any gate. Break something the agent can’t see, like a required environment variable, and watch the climb: four rungs, four log lines, and no human involved until the last one. Building this takes a few hours once, and the same dispatcher serves every loop you own after that.
Now go build something this weekend!
John Cook