Claude Code Stuck on Compacting Conversation: Cron Watchdog
If Claude Code is stuck on "Compacting conversation," stop the process. Then start a fresh session with a short written handoff instead of waiting or resuming. In an unattended claude -p run, wrap the command in a wall-clock timeout and use the PreCompact/PostCompact hooks to timestamp each compaction. If one runs too long, kill the run and retry clean. The rest of this guide covers why compaction stalls, what the real thresholds are, and how to make a cron job recover without you.
The official docs warn that /compact "reads the conversation it summarizes, so compacting a large context is itself a large request" (Claude Code docs: Manage costs). A large request can fail, get retried or run slowly, and in a headless run nobody sees the spinner.
Which option is right for you?
Pick based on how you run Claude Code today. Each row gives the first page to open.
| Your situation | What to pick first | First page to open |
|---|---|---|
| Beginner, interactive terminal, spinner frozen right now | Press Esc, then /compact with a focus, or /clear | Errors: Prompt is too long |
| Daily work: cron or CI job that sometimes never finishes | Wrapper script with a hard timeout and a compaction watchdog (below) | Run Claude Code programmatically |
| Long multi-hour agent tasks | Compact earlier with CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=50, and split work into sessions | Set the auto-compact window |
| Budget-sensitive (API key billing) | Cap each run with --max-budget-usd and --max-turns, and turn auto-compact off for short jobs | CLI reference |
| Not sure if it's compaction at all | Check the stream for api_retry events before blaming compaction | /why-claude-code-stops-mid-automation-and-how-to-fix-it/ |
Here's why each one fits. Interactive users can see and answer the error. Cron users can't, so they need a script that decides for them. Long tasks gain the most from compacting at a smaller, cheaper context. Budget users mainly need a ceiling so a stuck loop can't run up a bill.
Why does Claude Code get stuck on "Compacting conversation"?
Claude Code gets stuck when the summarization request fails, retries or loops. Most often the context is too big to summarize, or the API is slow or throttled.
The documented and reported causes fall into five groups:
- The summary doesn't fit. The errors page lists
Prompt is too long · automatic compaction failed: Compaction would make the conversation longer. That message means the summary would be bigger than the original, and the fix is/clear(Errors reference). The same page says a single-exchange conversation "cannot be compacted" at all. - Retries that look like a hang. Claude Code "retries transient failures up to 10 times with exponential backoff before showing you an error." Each attempt gets up to
API_TIMEOUT_MS(600000 ms, 10 minutes by default) to receive response headers (Errors: Automatic retries; Environment variables). A big compaction request that hits an overloaded API can therefore sit for a long time before it fails. - The retry watchdog you turned on. Setting
CLAUDE_CODE_RETRY_WATCHDOG=1makes Claude Code retry429and529errors indefinitely. On v2.1.199 and later it also raises the retry count for other transient errors to 300, "roughly three hours of backoff" (Errors: Tune retry behavior). That's useful for cron. It also means a compaction can wait hours unless you setCLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS. - Thrashing loops. GitHub issue #6004 describes an endless cycle: read files, compact, lose the state, then read the same files again. It was reported on v1.0.83, still seen on v2.0.6x, and closed as "not planned" in February 2026. The usual trigger is one large file or tool output that fills the context again right after each summary.
- Summarizer overflow (older versions). Issue #23751 reported
Conversation too longfailures at about 48% context usage on v2.1.32–v2.1.34, with image-heavy sessions hit hardest. It is marked fixed as of v2.1.85. If your cron box runs an old pinned version, update it first.
Here's why headless makes this worse. An interactive user sees the error and presses Esc. A claude -p process has no human, so a failed compaction ends in one of two ways. It either exits non-zero, which is easy to handle, or it keeps retrying or looping, which looks like a frozen job.
Example: a nightly job that reads a 10,000-line log file can fill most of the context in one tool call. Compaction summarizes it, the agent reads the log again, and the cycle repeats until the job hits a limit or you notice in the morning.
What triggers auto-compact, and can you change the threshold?
By default, Claude Code auto-compacts when the conversation reaches the model's context limit. You can lower that point with a setting, a CLI flag or an environment variable.
The "92–95%" figure you see quoted online doesn't match current docs. According to Default auto-compact thresholds (as of October 2026):
- Most sessions compact when the conversation reaches the model's context limit.
- Models with a native 1M window (Fable models, Sonnet 5 and later, Haiku 5.5, Opus 4.7 and later) compact "at about 967K tokens by default."
- Sonnet 4.6 and Opus 4.6 without extended context compact at the 200K boundary.
- With
CLAUDE_CODE_DISABLE_1M_CONTEXT=1, native-1M models compact at 200K. - Unrecognized model IDs, such as an LLM gateway alias, compact at whatever window Claude Code assumes for that ID.
The four ways to change it
| Where | How | Scope |
|---|---|---|
| In a session | /autocompact 500k (or /autocompact auto to reset) | Saved for the current model |
| Settings file | "autoCompactWindow": 200000 in ~/.claude/settings.json | Every model |
| One launch | claude --autocompact 500k (v2.1.221+) | That run only |
| Scripts/CI | CLAUDE_CODE_AUTO_COMPACT_WINDOW=200000 | Wins over the others |
Accepted values run from 100K to 1M tokens, and Claude Code caps the window at the model's context size (model config; settings reference).
Use CLAUDE_AUTOCOMPACT_PCT_OVERRIDE to compact earlier as a percentage. The docs say "the variable can't raise the threshold, so values above the default percentage are ignored" (Environment variables). It only applies in sessions that compact before the model's context limit. So pair it with CLAUDE_CODE_AUTO_COMPACT_WINDOW if you want it to take effect.
Turning it off
Set "autoCompactEnabled": false in a settings file (settings reference). With auto-compact off, a full context produces a Context limit reached error instead of a compaction (Errors). For a short cron job that's often what you want: a fast, loud failure your script can catch. For long jobs, it means the run just stops at the limit.
There's also a hook-level off switch. A PreCompact hook that exits with code 2 blocks compaction (Hooks reference). See /claude-code-hooks-lifecycle-every-event-explained/ for where it fires in the lifecycle.
How do you detect a stuck compaction in a headless or cron run?
To spot a stuck compaction in headless Claude Code, mark when each compaction starts and ends. If a start has no matching end within your limit, the run is stuck.
Exit codes alone aren't enough. The docs say Claude Code "exits with code 0 on success and a non-zero code when the run fails" (Headless docs). A hung process never exits, though, so you also need signals that work while it's still running. There are three:
- Compaction markers from hooks.
PreCompactfires "before context compaction begins" andPostCompactfires "after context compaction completes successfully." Both accept anautoormanualmatcher (Hooks reference). Have the first write a timestamp file and the second delete it. A timestamp file older than your limit means compaction is stuck. - Stream inactivity. Run with
--output-format stream-json --verboseand write stdout to a file. If the file hasn't changed in N minutes, something is wrong. Note thatPreCompactandPostCompact"never produce ahook_startedevent" in the stream (CLI reference), so the hook marker files are the more reliable signal. - Retry events. Before each retry, the stream includes a
system/api_retryevent withattempt,max_retries,retry_delay_msand anerrorcategory such asrate_limitoroverloaded(Headless docs). Several of these right after the marker file appears means compaction is fighting the API rather than looping.
Steps to add compaction markers
- Create a job folder, for example
mkdir -p ~/jobs/nightly/state. - Save this as
~/jobs/nightly/compact-watch.json. It's an example; change the paths to match yours:
{
"hooks": {
"PreCompact": [
{ "matcher": "auto",
"hooks": [{ "type": "command",
"command": "date +%s > \"$HOME/jobs/nightly/state/compact.started\"" }] }
],
"PostCompact": [
{ "matcher": "auto",
"hooks": [{ "type": "command",
"command": "rm -f \"$HOME/jobs/nightly/state/compact.started\"; date >> \"$HOME/jobs/nightly/state/compact.log\"" }] }
]
}
}
- Pass it with
--settings ./compact-watch.json. In--baremode Claude Code skips hooks it would normally discover on disk. Settings you pass with--settingsare still loaded (Headless: bare mode). - Watch the
state/folder from your wrapper script (next section).
A common mistake is putting the hooks only in .claude/settings.json and then running with --bare. The hooks silently never fire. The fix is to pass them through --settings.
How do you set a timeout and kill a hung Claude Code process?
Claude Code has no built-in wall-clock limit for claude -p. Wrap it in timeout or a watchdog loop, send SIGTERM first, then SIGKILL after a grace period.
What the docs give you for runaway work: --max-turns "exits with an error when the limit is reached" and has no limit by default. --max-budget-usd stops the run once estimated spend reaches the cap (CLI reference). Neither one measures time. A single compaction that keeps retrying uses no extra turns, so you need a clock as well.
Option A: GNU timeout (Linux)
timeout -k 30s 90m claude -p "$(cat task.md)" --output-format json --max-turns 40
echo "exit=$?"
GNU timeout sends TERM when the limit is reached and KILL 30 seconds later because of -k. It exits with status 124 when the command timed out (GNU coreutils: timeout invocation).
Here's what happens inside Claude Code on SIGTERM: "Claude Code exits with code 143," leaves the current turn unfinished, kills any running Bash process tree and runs SessionEnd hooks (Headless: Stop a run with SIGTERM). If you'd rather end the turn cleanly, the docs say to send SIGINT first.
Option B: watchdog loop (works anywhere, catches compaction specifically)
This example script combines a total time limit, a compaction limit and a stream-inactivity check. The limits are example values; tune them to your job.
#!/usr/bin/env bash
# run-agent.sh — example watchdog for one unattended Claude Code run
set -u
JOB="$HOME/jobs/nightly"; STATE="$JOB/state"
COMPACT_LIMIT=900 # example: 15 min max for one compaction
HARD_LIMIT=5400 # example: 90 min for the whole run
IDLE_MIN=20 # example: no stream output for 20 min = stalled
mkdir -p "$STATE"; rm -f "$STATE/compact.started"
cd "$JOB/repo" || exit 1
claude -p "$(cat "$JOB/task.md")" \
--settings "$JOB/compact-watch.json" \
--output-format stream-json --verbose \
--max-turns 40 --permission-prompts none \
> "$STATE/stream.jsonl" 2> "$STATE/stderr.log" &
PID=$!; START=$(date +%s)
stop() { echo "$(date) $1" >> "$STATE/watchdog.log"
kill -INT "$PID" 2>/dev/null; sleep 10
kill -TERM "$PID" 2>/dev/null; sleep 20
kill -KILL "$PID" 2>/dev/null; exit "$2"; }
while kill -0 "$PID" 2>/dev/null; do
sleep 30; NOW=$(date +%s)
if [ -f "$STATE/compact.started" ] && \
[ $((NOW - $(cat "$STATE/compact.started"))) -gt "$COMPACT_LIMIT" ]; then
stop "compaction stalled" 75
fi
[ $((NOW - START)) -gt "$HARD_LIMIT" ] && stop "hard limit" 124
[ -n "$(find "$STATE/stream.jsonl" -mmin +"$IDLE_MIN")" ] && stop "stream idle" 76
done
wait "$PID"; exit $?
The exit codes 75 and 76 are this script's own convention, so your retry layer can tell a stalled compaction apart from a normal failure. --permission-prompts none (v2.1.259+) stops the run from waiting on a permission prompt nobody will answer (Headless docs). That's a separate kind of hang that looks the same from outside.
Pitfalls
- Killing too early. A normal compaction of a large context takes real time, and each API attempt can wait up to 10 minutes for headers by default. A 2-minute limit will kill healthy runs. Start around 15 minutes and look at
compact.logto see how long yours actually take. - Mistaking background wait for a hang. After the final turn,
claude -pcan wait up to 10 minutes for background subagents or commands (CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS, default 600000) (Environment variables). If stderr showsWaiting for background work to finish, it isn't compaction. - Killing only the parent. If you launch through
bash -cornpx, signal the process group. Otherwise a childclaudecan keep running.
How do you avoid hitting compaction at all?
Keep each Claude Code session well below the auto-compact window. Filter big tool outputs, send verbose work to subagents, and give each task its own session.
These are the levers that matter most for unattended jobs, roughly in order of payoff:
- One job, one session. Start every cron run fresh (no
--continue). The docs recommend/clear"when switching to unrelated work," and a new-pprocess does the same thing (Context window docs). - Filter output before Claude sees it. The docs' own example is a
PreToolUsehook that rewrites test commands to keep only failures. They point out that grepping a 10,000-line log forERRORcan cut context "from tens of thousands of tokens to hundreds" (Manage costs). Copy-paste versions are in /claude-code-hooks-examples-7-copy-paste-scripts/. - Delegate large reads to subagents. Their file contents stay in their own context window, and only a summary comes back (Context window docs).
- Use
--barefor scripts. The docs say "--bareis the recommended mode for scripted and SDK calls" (Headless docs). It skips CLAUDE.md, MCP servers, skills and auto memory, so the run starts with less context. Bare mode needsANTHROPIC_API_KEY, because it doesn't use your subscription login. - Trim always-loaded files. Keep CLAUDE.md under 200 lines and move workflow-specific instructions into skills, which load only when invoked (Manage costs).
- Compact earlier if you must compact. A summary of a 150K context is a smaller request than a summary of 950K. On a native-1M model, example settings like
CLAUDE_CODE_AUTO_COMPACT_WINDOW=300000keep each compaction request moderate.
To measure what's filling the context, run /context in an interactive session on the same repo. It "shows your conversation size in tokens" broken down by category (Errors). /claude-code-how-to-see-token-usage-and-reduce-it/ walks through reading that output.
A common mistake is asking an agent to "read all files in src/ and summarize". That's the quickest way to fill the window. Name the files instead, or have a subagent do the reading.
How do you resume a session safely after a compaction failure?
After a failed compaction, don't resume the stuck Claude Code session. Start a new session seeded with a short handoff file, and keep the old transcript for debugging.
Here's why. --resume and --continue reload the stored conversation. That's the same oversized history that just failed to compact, so the next turn is likely to trigger the same compaction. Two details make this worse after a kill:
- After SIGTERM, "Claude Code leaves the interrupted turn as it is, and your next prompt drives the conversation." Setting
CLAUDE_CODE_RESUME_INTERRUPTED_TURN=1makes it continue the interrupted turn instead (Headless docs). That's the opposite of what you want here. - If you resume with different system-prompt flags, they only take effect "once the conversation is compacted or when you start a new conversation" (CLI reference).
Safer recovery steps
- Record the failed session ID. With
--output-format stream-json, thesystem/initevent and the finalresultcarrysession_id. Save it tostate/last_sessionfrom your wrapper. - Keep a progress file the agent writes. Add this line to
task.md(example wording): "After each completed step, append one line to PROGRESS.md: step, files changed, what's next." - Start fresh with a handoff. Example:
claude -p "Continue the task in task.md. Completed steps are in PROGRESS.md; verify the last one, then continue." --settings compact-watch.json - Only resume if you need the history. Fork it so the original stays intact:
claude --resume "$ID" --fork-session(CLI reference). Then run/compactwith a narrow focus, for example/compact keep only the list of changed files and the failing test name. - Check the code state. A killed run may have left half-written files.
git statusandgit stashhandle most cases. /claude-code-checkpoints-how-rollback-works-in-automations/ explains what Claude Code's own checkpoints do and don't cover.
In an interactive terminal, the errors page and community reports give a shorter path. Press Esc twice to step back a few messages, then run /compact again. If that still has no room, use /clear; the old conversation stays reachable through /resume (Errors).
How do you build auto-recovery for unattended runs (cron, CI)?
A watchdog kills the stalled Claude Code run, and a wrapper retries it once in a fresh session. The wrapper also sorts exit codes and logs anything it can't fix.
Building on the watchdog script above, a minimal recovery layer looks like this (example):
#!/usr/bin/env bash
# nightly.sh — called from cron
JOB="$HOME/jobs/nightly"
for attempt in 1 2; do
"$JOB/run-agent.sh"; code=$?
case $code in
0) echo "$(date) ok attempt=$attempt" >> "$JOB/state/history.log"; exit 0 ;;
75|76|124|143) # stalled compaction, idle stream, hard limit, SIGTERM
echo "$(date) stalled code=$code attempt=$attempt" >> "$JOB/state/history.log"
mv "$JOB/state/stream.jsonl" "$JOB/state/stream.$(date +%s).jsonl" ;;
*) echo "$(date) failed code=$code" >> "$JOB/state/history.log"; break ;;
esac
done
exit 1
And the crontab line, for example: 15 2 * /home/you/jobs/nightly/nightly.sh >> /home/you/jobs/nightly/state/cron.log 2>&1. Environment details, such as PATH and auth tokens under cron, are covered in /claude-code-cron-job-setup-run-automations-overnight/.
Design decisions that matter
- Retry once, not forever. If the second fresh session also stalls at compaction, the task itself is too big for one session. Retrying again just burns tokens. Issue #6004 includes one user's report of $800 in API charges from a compaction loop (GitHub #6004).
- Bound the API retry wait. If you use
CLAUDE_CODE_RETRY_WATCHDOG=1to ride out rate limits, also setCLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS(v2.1.295+). Otherwise a single compaction request can wait out429/529errors without limit (Errors). Rate-limit handling for cron is covered in /claude-code-rate-limit-reached-retry-setup-for-cron-jobs/. - Cap money as well as time. Add
--max-budget-usd(print mode only). The docs warn spend "can pass the cap, so leave headroom" (CLI reference). - Alerting is up to you. Claude Code doesn't page anyone. Have the wrapper's failure branch call whatever you already use: a Slack incoming webhook via
curl,mail, or your CI's failure notification. - Lower retries if you want fast failure.
CLAUDE_CODE_MAX_RETRIESdefaults to 10. The docs suggest you "lower it to surface failures faster in scripts" (Errors).
A common mistake is letting the retry layer use --continue. The retry then inherits the same bloated context and stalls the same way. Always retry with a new session plus the progress file.
Claude Code auto-compact vs manual context management: which is more reliable for long runs?
For unattended long runs, manual context management in Claude Code is more predictable. Auto-compact is convenient, but it adds a failure point your script can't control.
| Approach | How you set it | Strength | Limitation | Best for |
|---|---|---|---|---|
| Default auto-compact | Nothing | Zero setup, the run continues past the limit | Compaction happens at the largest, most expensive context, so it's more exposed to slow or failed requests | Interactive work |
| Earlier auto-compact | CLAUDE_CODE_AUTO_COMPACT_WINDOW, CLAUDE_AUTOCOMPACT_PCT_OVERRIDE | Smaller summary requests | Compacts more often; each summary can drop detail | Multi-hour single tasks |
| Auto-compact off + chunked sessions | "autoCompactEnabled": false, one task per -p run, PROGRESS.md handoff | Deterministic: a full context becomes a clean error | You design the task split up front; a run can stop at the limit | Cron jobs with clear steps |
| Subagent isolation | Delegate reads/tests to subagents | Main context stays small | Subagents have their own windows and can compact too (sub-agents docs) | Research-heavy jobs |
| Block via hook | PreCompact exits 2 | Fine-grained control, e.g. block only during a critical step | Blocking leaves you at the limit with no relief | Rare, advanced cases |
My pick for the reader of this guide: for recurring jobs, use chunked sessions with auto-compact either off or set to a lower window, plus the watchdog. Here's why. A job that fails loudly at step 3 of 5 is easy to resume from PROGRESS.md. A job that freezes in a summary at 2 a.m. costs you a night.
One pitfall with turning compaction off: if a single step needs more context than the window holds, it will always fail. Split that step further, or move its large reads into a subagent. /ai-agent-vs-automation-a-5-question-test-for-claude-code/ helps decide whether a step needs an agent at all.
Next steps
Do today (DIY):
- Run
claude --versionon the machine that runs your cron. If it's older than v2.1.85, update withclaude update. The summarizer overflow bug in issue #23751 was fixed in that version. - Add
compact-watch.jsonandrun-agent.shfrom this guide to one job, with generous limits (15 min per compaction, 90 min total). Run it for a week and readcompact.logto see real compaction durations. - Add a "write one line to PROGRESS.md per step" instruction to your task prompt, so any kill can be retried in a fresh session.
If you'd rather not do it yourself: Anthropic's official Claude Code GitHub Actions integration runs Claude Code inside a CI job. The CI runner gives you job timeouts and failure notifications, so you don't maintain a cron box. It suits people already on GitHub. It doesn't suit jobs that need your local machine or private network, and you still pay for Actions minutes plus Claude usage.
If your real problem is hitting usage limits rather than hangs, Claude subscriptions include Claude Code. Pro is "$20 if billed monthly" and Max starts "from $100 per month" (as of October 2026; check the official pricing page). A bigger plan doesn't fix a stuck compaction, though. It only gives you more headroom before rate limits slow the retries.
Related guides on this site:
- Claude Code Cron Job Setup: Run Automations Overnight
- Why Claude Code Stops Mid-Automation (and How to Fix It)
- Claude Code Hooks Lifecycle: Every Event, Explained
- Claude Code Permissions Best Practices: Headless Template
FAQ
Why is Claude Code frozen on compacting conversation?
Usually the summary request is very large and is either retrying against a slow or throttled API, or caught in a re-read-and-compact loop. Each attempt can wait up to 10 minutes for response headers by default, and Claude Code retries up to 10 times (Errors). In a terminal, press Esc and run /compact with a focus, or /clear.
How do I force Claude Code to stop compacting?
Interactively, press Esc to interrupt, then /clear for a fresh context. In scripts, send SIGINT or SIGTERM to the process; SIGTERM exits with code 143 (Headless docs). To prevent it, a PreCompact hook that exits with code 2 blocks compaction.
Does Claude Code auto-compact affect cron jobs?
Yes. claude -p runs use the same auto-compact thresholds as interactive sessions, but nobody sees the spinner. A stalled compaction holds the job until something kills it. Add a wall-clock timeout and hook-based compaction markers so the job fails and retries on its own.
Can I disable auto-compact in Claude Code?
Yes. Set "autoCompactEnabled": false in a settings file (settings reference). After that, a full context ends with a Context limit reached error instead of compacting. That suits short, chunked cron jobs better than long single tasks.
What exit code does Claude Code return on timeout?
Claude Code has no built-in time limit for -p, so the code depends on what stopped it. GNU timeout returns 124, SIGTERM gives 143, and --max-turns exits with an error status when the turn limit is hit (GNU timeout; Headless docs; CLI reference).
Sources
- Claude Code docs: Run Claude Code programmatically (headless)
- Claude Code docs: CLI reference
- Claude Code docs: Model configuration — auto-compact window and default thresholds
- Claude Code docs: Environment variables
- Claude Code docs: Settings reference
- Claude Code docs: Hooks reference
- Claude Code docs: Errors, automatic retries and retry tuning
- Claude Code docs: Explore the context window
- Claude Code docs: Manage costs effectively
- Claude Code docs: GitHub Actions
- Claude Code docs: Subagents
- GitHub issue #6004: Infinite compaction loop
- GitHub issue #23751: Compaction fails with "Conversation too long"
- GNU coreutils: timeout invocation
- Claude pricing
- ClockedCode: Claude Code Stuck on Compacting
- Du'An Lightfoot: What to Do When Claude Code Starts Compacting
Spotted something out of date or wrong? Tell us at shigoto.ai.note@gmail.com. Guides are re-checked against official sources on a rolling schedule.