◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-04-23Advanced

Finishing Long-Running Claude Code Tasks: A Resilience Playbook You Can Ship

Multi-hour Claude Code jobs — bulk refactors, TypeScript migrations, mass test generation — always stop before they finish, and recovery is painful when you cannot tell what already ran. This guide ships concrete patterns: a checkpoint-driven manifest, a three-state circuit breaker, idempotent retry rules, and a freeze-and-resume protocol you can copy into your repo today.

Claude Code264long-running taskstroubleshooting92operations30resilience10circuit breaker

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 log is 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)" --quiet

Claude 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.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⟐ Claude Code2026-10-12
After Moving to SPM, My App Died the Moment It Launched — Reading One dyld Line with Claude Code
Right after moving from CocoaPods to Swift Package Manager, the build passed but the app died on launch. Here is how I traced one dyld line to its cause, the fix, and how to verify it.
⟐ Claude Code2026-10-05
I Wrote "I'll Do the Commits" in CLAUDE.md. The Next Morning git log Had One More Line.
When Claude Code commits despite being told not to: undo with git reset --soft, then stop it physically with permissions.deny plus a PreToolUse hook. Covers the git -c form that deny rules let through, and how to test the hook without starting Claude Code.
⟐ Claude Code2026-09-22
Taking Inventory of Worktrees That Were Supposed to Be Gone
Isolated worktrees my subagents created were never folded away. Here is what I measured in git 2.34.1 about when remove succeeds, the branches that outlive the trees, the 12 MB per-tree difference, and how I decide who owns cleanup.