CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
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 Code197long-running taskstroubleshooting87operations15resilience10circuit 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-07-14
An Empty Variable and rm -rf: How Claude Code's Auto Mode Preflight Saved My Late-Night Cleanup
One night I let Claude Code sweep the build caches out of several app repositories at once, and an empty variable nearly turned a targeted cleanup into a wide delete. Here is the field record of being saved by auto mode's rm -rf preflight, and the confirmation rules I built afterward.
Claude Code2026-07-01
My Claude Code Hooks Stopped Firing After an Update — the Hyphenated Matcher Exact-Match Change in v2.1.195
In Claude Code v2.1.195, hook matchers containing a hyphen switched from partial match to exact match, silently disabling an existing PreToolUse hook. Here is how I isolated the cause and how to write matchers that won't break.
Claude Code2026-06-17
When an Announced Billing Change Gets Paused at the Last Minute: Designing Automation That Doesn't Rush the Cutover
A billing change that was supposed to take effect on June 15 was paused that same day. If your pipeline trusts the announced date, a retraction breaks it twice. Here is a design that decides the cutover from a runtime signal, with implementation code.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →