You hand Claude Code a multi-hour task — a 500-file refactor, a TypeScript migration, generating tests across a monorepo — and it stops partway through. The chat buffer is ambiguous about how far it got. You do not know whether to resume, re-run, or start over. A day of work is now a triage exercise.
Running jobs like this reliably is not about getting better recovery prompts. It is about designing the task from day one to assume it will stop. This playbook walks through the patterns I rely on in production — a checkpoint manifest, a circuit breaker, idempotent retries, and a freeze protocol — with copy-pasteable code.
The four stop modes (and why one fix does not cover all of them)
Grouping every "Claude Code stopped" failure together is why the fixes do not stick. There are four distinct modes.
Mode 1 — output token or context ceiling. A single turn maxes out its output budget, or the whole session fills the context window and quality degrades until Claude forgets the task. Fix: split prompts; shorten the per-turn scope; start new sessions at natural boundaries.
Mode 2 — API rate limits and transient errors. Anthropic returns a 429 or a 503, Claude Code auto-retries, but sustained failures eventually halt the run. External factor. Fix with retry strategy + circuit breaker.
Mode 3 — local environment hit a wall. Disk full, process OOM, shell session dropped, SSH tunnel timed out. Claude Code "went silent" but the real culprit is your laptop or VM. Fix with environment monitoring, not prompt changes.
Mode 4 — scope design error. The most common. You asked for "one file at a time," but Claude Code pulled in the whole dependency graph to reason about imports and blew context in the first hour. Or you bundled npm test into each step and every edit now waits for a 4-minute suite. Fix with better scope engineering.
A real production setup addresses all four separately. Most "resilience" tips in the wild conflate them.
Checkpoint-driven design: put state on disk, not in memory
The single most impactful pattern I use is to stop relying on Claude Code's session memory for progress, and instead write progress to a JSON manifest on disk.
The minimal manifest
{
"task_id": "typescript-migration-2026-04",
"total_items": 487,
"completed": [
{ "item": "src/user/profile.js", "finished_at": "2026-04-23T09:12:31Z", "status": "ok" },
{ "item": "src/user/settings.js", "finished_at": "2026-04-23T09:13:02Z", "status": "ok" }
],
"failed": [
{ "item": "src/legacy/auth.js", "reason": "ambiguous imports", "last_tried_at": "2026-04-23T09:15:48Z" }
],
"queued": [
"src/legacy/rbac.js",
"src/legacy/session.js"
]
}In the opening prompt, declare the contract explicitly: "Update this manifest the moment each item finishes, and commit. If you are restarted, read this file and resume from the head of queued."
Why this works where session memory does not
When Claude Code stops, the manifest still exists. On resume you only need to instruct: "Read task_manifest.json and start from the first entry in queued." You get an exact resume point instead of a paragraph of guesswork.
Two design rules make the difference:
- Update after every single item, not every batch. Batch updates lose progress if you stop mid-batch.
- Bind the update to a git commit. If the manifest is corrupted,
git logis your source of truth.
Bash wrapper for the update step
#!/usr/bin/env bash
# mark_done.sh — record one item and commit
set -euo pipefail
MANIFEST="./task_manifest.json"
ITEM="$1"
STATUS="${2:-ok}"
python3 - "$MANIFEST" "$ITEM" "$STATUS" << 'PY'
import sys, json, datetime
manifest_path, item, status = sys.argv[1], sys.argv[2], sys.argv[3]
with open(manifest_path) as f:
m = json.load(f)
m["completed"].append({
"item": item,
"finished_at": datetime.datetime.utcnow().isoformat() + "Z",
"status": status
})
if item in m["queued"]:
m["queued"].remove(item)
with open(manifest_path, "w") as f:
json.dump(m, f, indent=2, ensure_ascii=False)
PY
git add "$MANIFEST"
git commit -m "progress: $ITEM ($STATUS)" --quietClaude Code calls ./mark_done.sh src/user/profile.js after each item. Your git log becomes a line-by-line audit trail.
A three-state circuit breaker
Runaway retries against a failing API will happily burn through your quota. A circuit breaker is the classic fix from distributed systems, and it transplants cleanly here.
The states
- Closed — normal operation, retry on failure up to a threshold
- Open — too many consecutive failures; refuse all calls for a cooldown window
- Half-open — cooldown elapsed, allow one probe; if it succeeds, go Closed; if it fails, back to Open
Reference implementation
import time, json, pathlib
STATE_PATH = pathlib.Path("./cb_state.json")
THRESHOLD = 3 # consecutive failures before opening
COOLDOWN_SEC = 600 # seconds to stay open
def load_state():
if not STATE_PATH.exists():
return {"state": "closed", "fail_count": 0, "opened_at": 0}
return json.loads(STATE_PATH.read_text())
def save_state(s):
STATE_PATH.write_text(json.dumps(s, indent=2))
def allow_call():
s = load_state()
if s["state"] == "closed":
return True
if s["state"] == "open":
if time.time() - s["opened_at"] >= COOLDOWN_SEC:
s["state"] = "half_open"
save_state(s)
return True
return False
return True # half_open
def record_success():
save_state({"state": "closed", "fail_count": 0, "opened_at": 0})
def record_failure():
s = load_state()
s["fail_count"] += 1
if s["fail_count"] >= THRESHOLD:
s["state"] = "open"
s["opened_at"] = time.time()
save_state(s)Hook allow_call / record_success / record_failure around the call Claude Code makes. A threshold of 3 with a 10-minute cooldown is conservative — transient 503s self-heal, real outages stop burning tokens.
Idempotency is the price of admission
None of this helps if rerunning a unit corrupts state. Every item must be safe to run twice.
Three rules that get you there:
Check the current state before acting. If you are "converting a file to TypeScript," inspect the extension and import style first and skip if already converted. Trusting only the manifest's completed list breaks if you stop between the update and the commit.
Write all outputs to attempt-scoped paths. Logs and intermediate artifacts live at logs/{task_id}/{item}_{attempt}.log. Overwriting a stable path destroys the evidence you need for the next attempt.
Defer shared-resource changes to a final commit phase. DB migrations, key issuance, outbound notifications — anything that is hard to make idempotent — runs in a single finalization step after all items succeed. Never leave half-completed side effects behind.
Freeze-and-resume: preserving partial work
When the breaker trips or no response has come back for 20 minutes, automate a freeze.
#!/usr/bin/env bash
# freeze.sh — snapshot current working state to a freeze branch
set -euo pipefail
TASK_ID=$(python3 -c "import json; print(json.load(open('task_manifest.json'))['task_id'])")
FREEZE_BRANCH="freeze/${TASK_ID}-$(date +%Y%m%d-%H%M%S)"
git checkout -b "$FREEZE_BRANCH"
git add -A
git commit -m "freeze: ${TASK_ID} at $(date -Iseconds)" --allow-empty
git push origin "$FREEZE_BRANCH"
echo "Frozen to $FREEZE_BRANCH"Keep freeze branches off main. On resume, cross-check against the manifest: if the freeze is consistent, rebase it onto main; if not, discard and restart from queued.
Pair the freeze with a notification so a human actually finds out:
curl -X POST -H 'Content-Type: application/json' \
-d "{\"text\": \"Circuit opened on ${TASK_ID}. Check freeze/${TASK_ID}-*.\"}" \
"$SLACK_WEBHOOK_URL"Silent stops waste hours before anyone notices.
The design checklist I run every long task through
Before kicking off any multi-hour Claude Code job I run this five-item gate:
- Is each unit of work scoped to 30 seconds to 5 minutes? Larger units inflate failure cost.
- Is each unit idempotent — or made idempotent by an explicit state check before running?
- Does progress live in a manifest that survives a crash?
- Is there a failure notification path? Silent stops get discovered hours too late.
- Are the circuit breaker threshold and cooldown written down, not implicit?
If a task fails this checklist, I redesign the task before touching a prompt. Every time I have skipped this check, I have paid for it.
Start with one thing
You do not need to implement all of this at once. The one pattern with the best return on effort is the manifest-driven progress tracker. One JSON file and one Bash wrapper gives you a clean resume point and a commit-level audit trail, which together solve maybe 70% of the pain.
Add the circuit breaker next. Freeze protocol and idempotency tightening come after that, once you see where your own pipeline actually breaks.
Stops will still happen. The goal is not a task that never stops — it is a stop you can recover from without losing a day.