Free playbooks in your inbox

Why Claude Code Fails Overnight

Find out in one command why last night's Claude Code loop failed and what to do about it. There are 6 ways a loop fails, each leaves a trace in the loop's own result files, and the most common fake success comes from a check that exits 1.

From the youcanbuildthings catalog ▸ Build-tested

Hello builders,

When a Claude Code loop stops overnight, one command can tell you which way it failed and what to do next, and that is what makes running more than one loop at a time manageable. The expensive case is the run that reported success: every signal says healthy, and the bad output is sitting in the runs you already accepted. There are six Claude Code loop failure modes worth naming, each one leaves a signature in records your loop already writes, and a short script can read them for you.

The sharpest dismissal of this whole subject has 56 upvotes: “Loop engineering doesn’t exist. It’s just scheduled jobs and event triggers that make an agent go brrrr.” I think he’s right that plenty of what gets called a loop is a cron job with a better label. What he leaves out is that the thing inside a loop can’t be trusted to report its own results, and a cron job never had that problem, which is why these six look nothing like a cron job’s failures.

Researchers have counted this too. The MAST taxonomy was developed on 150 traces with inter-annotator agreement of kappa = 0.88, identifies 14 unique modes and ships a dataset of 1600+ annotated traces across 7 frameworks, clustered into three categories:

  • system design issues
  • inter-agent misalignment
  • task verification

What does a kappa of 0.88 buy you? It means two people reading the same broken trace almost always named the same mode, and one of the three categories is exactly the job a loop’s gate exists to do.

Six modes, six signatures

So what broke last night? I sort every bad run into one of six modes, and each one leaves a mark in the records we already keep.

Claude Code loop failure modes table with signature, guard and what to do now for six modes: non-termination, self-locking repetition, false-green completion, cascading bad context, unreviewable output volume and silent scope drift.

  • Non-termination: error_max_turns with the progress line still moving; raise the cap or shrink the task
  • Self-locking repetition: identical verifier feedback twice; do not rerun, go to a fresh context
  • False-green completion: subtype: success with no verdict on disk; check the exit code first, every time
  • Cascading bad context: state entries with no evidence citation; delete the uncited entries
  • Unreviewable output volume: more insertions than you will genuinely read; halve the batch and rerun
  • Silent scope drift: files changed outside the plan’s paths; revert, then decide which was wrong

The guards behind those rows are, in order, an exit condition a computer evaluates, a stall rule that halts on identical feedback twice, a Stop gate on exit 2, a rule that every state entry cites the error behind it, nothing but a cap on batch size, and the scope-deny.txt your hook enforces.

False green is the expensive one, and it’s the one I hit first. Its most common cause is a gate written with exit 1, which Claude Code treats as a non-blocking error, so the hook fires, logs its objection, and the session ends anyway. When the diagnostic names it, fix the exit code and then work out how far back the gate was blind. If it was exiting 1 for a week, the bad output is in the six runs you already accepted.

Let the records talk

The design has one constraint: session transcripts under ~/.claude/projects/ are off limits, because Anthropic says that format is internal and changes between versions. So the script reads records your loop writes for itself, and if yours doesn’t write them yet, these lines in the wrapper do it each iteration:

claude -p --output-format json "$TASK" > ".loop/runs/$(date -u +%s).json"                 # the run's result
echo "$(date -u +%s) $(npm test --silent 2>&1 | grep -c '✓')" >> .loop/progress   # passing-test count

It also reads the verifier’s .loop/verdict.json with its advance field, the agent’s .loop/plan.md listing the paths it intends to touch, .loop/terminal, where a stall hook appends STALLED when the verifier’s feedback repeats, and .loop/state.md, whose - lines record approaches tried and rejected, each meant to cite the error that rejected it. Save this as loop-doctor, chmod +x it, and run it from the repo root after a bad night:

#!/usr/bin/env bash
# loop-doctor: names the failure mode from the loop's own records
set -uo pipefail
last=$(ls -1t .loop/runs/*.json 2>/dev/null | head -1)
[ -z "$last" ] && { echo "no runs recorded"; exit 1; }
subtype=$(jq -r '.subtype // "unknown"' "$last")
say() { printf '%-26s %s\n   guard: %s\n' "$1" "$2" "$3"; }
prog=$(tail -3 .loop/progress 2>/dev/null | awk '{print $2}' | sort -u | wc -l | tr -d ' ')

case "$subtype" in
  error_max_turns|error_max_budget_usd|error_wall_clock)
    if grep -q '^STALLED' .loop/terminal 2>/dev/null || { [ -s .loop/progress ] && [ "${prog:-9}" -le 1 ]; }; then
      say "SELF-LOCKING REPETITION" "hit a ceiling after identical feedback or flat progress" "stall rule, do not wait for the cap"
    else
      say "NON-TERMINATION" "stopped on $subtype, progress still moving" "raise the cap or shrink the task"
    fi ;;
  success)
    if [ ! -f .loop/verdict.json ] || [ "$(jq -r '.advance' .loop/verdict.json)" != "true" ]; then
      say "FALSE-GREEN COMPLETION" "success with no passing verdict on disk" "Stop gate must exit 2, not 1"
    else
      paths=$(grep -oE '[A-Za-z0-9_.-]*[./][A-Za-z0-9_./-]*' .loop/plan.md 2>/dev/null)
      # in scope: exact path, matching bare filename, or under a planned dir/
      drift=$(git diff --name-only HEAD 2>/dev/null | grep -v '^\.loop/' | awk 'NR==FNR{p[$0];next}
        {n=split($0,a,"/"); for(k in p) if(k!="" && ($0==k || (k!~/\// && a[n]==k) || (k~/\/$/ && index($0,k)==1))) next; print}' \
        <(printf '%s\n' "$paths") -)
      big=$(git diff --shortstat HEAD 2>/dev/null | grep -oE '[0-9]+ insert' | grep -oE '[0-9]+' || echo 0)
      if [ -n "$drift" ]; then say "SILENT SCOPE DRIFT" "touched: $(echo $drift)" "scope-deny.txt, or widen the plan"
      elif [ "${big:-0}" -gt 400 ]; then say "UNREVIEWABLE OUTPUT VOLUME" "$big insertions in one unit" "cap the batch size"
      else echo "no failure signature found"; fi
    fi ;;
  *) echo "unrecognised ending: $subtype, read this run's record" ;;
esac

if grep -q '^- ' .loop/state.md 2>/dev/null && ! grep -qE '\.(js|ts|py|go|rs|rb|java):|expected|got ' .loop/state.md; then
  say "CASCADING BAD CONTEXT (warn)" "rejected approaches with no evidence" "cite the error on every entry"
fi

We test it the way we test any gate, by breaking things on purpose. Set --max-turns 2 on a task that needs ten, flip your Stop gate from exit 2 to exit 1 and let the agent declare victory, then point the loop at a fix that lives outside the plan’s paths. Three runs should give us three correct names, without opening a single log.

It has two honest limits. It can’t see a run that never wrote a record, so we check that every exit path in our wrapper writes one. And no failure signature found only means none of the six matched, so work that is subtly wrong and still passes its tests lands there every time, which is why your gate matters more than your diagnostic.

The flags will change long before those six signatures do, so it’s one we keep.

Now go build something this weekend!

John Cook

Why trust this? Every youcanbuildthings guide is pulled from a build-tested book: code that ran in production before it was written down.