Headless Notes

Practical guides to running AI automations that keep working while you sleep.

Why Claude Code Stops Mid-Automation (and How to Fix It)

Why Claude Code Stops Mid-Automation (and How to Fix It)

2026-10-13 · 10 min read

#claude code #headless #automation #agent sdk

A headless Claude Code run (claude -p) usually stops for one of three reasons. It reached a limit you set, such as --max-turns or --max-budget-usd. A timer ran out, such as the 2-minute Bash default, the 10-minute background-wait cap or your CI job's own timeout. Or it needed a permission that nobody was there to give, so the action was denied and Claude gave up on that step. The fix is to set each of these on purpose, read the result subtype and exit code after every run, and give the agent a clear, pre-approved set of tools.

This guide goes through each trigger using Anthropic's own documentation, then ends with a checklist you can copy into your scheduled jobs.

How to see why a run stopped

Before changing any settings, make every run report why it ended. Claude Code exits with code 0 on success and a non-zero code when the run fails. One catch: when the failure happens inside the run (missing authentication, for example), the failure is printed as the result on stdout, not on stderr. If your cron script only logs stderr, you will miss it.

Use JSON output so you can read the result in your script:

claude -p "Triage new issues" --output-format json > run.json
jq -r '.subtype, .stop_reason, .total_cost_usd, .num_turns' run.json

The Agent SDK docs list these result subtypes: success, error_max_turns, error_max_budget_usd, error_during_execution and error_max_structured_output_retries. Every result includes total_cost_usd, usage, num_turns and session_id. The stop_reason field can also be max_tokens, which means the model hit its output token limit on the last turn.

For more detail, use --output-format stream-json --verbose. In that format, permission denials show up as permission_denied messages, and the final result lists them in permission_denials. Retryable API errors show up as system/api_retry events with a category such as rate_limit, overloaded, authentication_failed or max_output_tokens (source).

Trigger 1: Turn and token budgets

Max turns

--max-turns limits the number of agentic turns in print mode. It has no limit by default and exits with an error when the limit is reached. A turn is one tool-use round trip, so a task like "run tests, read files, edit, re-run tests" uses several. If you set --max-turns 3 to keep a nightly job small, it can stop before the edit step. The SDK docs give this same example, where max_turns=2 stops before the edit.

Fix: Give the job some room above the number of turns it normally needs, and treat error_max_turns as "resume later", not as a failure. The session ID still comes back, so you can run claude -p "continue" --resume "$session_id" (source).

Dollar budget

--max-budget-usd stops the run once estimated spend reaches the amount you set. Things to know:

Output tokens and context

CLAUDE_CODE_MAX_OUTPUT_TOKENS sets the maximum output for most requests. Defaults and caps depend on the model, and raising it reduces the effective context window available before auto-compaction. If a long file write ends with stop_reason: max_tokens, this setting is the place to look.

Long runs also fill up the context window. When it gets close to the limit, Claude Code summarizes older history, so instructions from early in the conversation may not be kept. If a long job seems to "forget" its rules partway through, that is often why. Put rules that must always apply in CLAUDE.md, or, with --bare, in --append-system-prompt-file. Keep tool output small: Bash output read back into context is capped by BASH_MAX_OUTPUT_LENGTH (default 30,000 characters) (source).

Trigger 2: Session and tool timeouts

Several timers apply to a headless run:

TimerDefaultWhat happensSetting
Foreground Bash command2 minCommand times outBASH_DEFAULT_TIMEOUT_MS
Max Bash timeout Claude can request10 minCeiling for long commandsBASH_MAX_TIMEOUT_MS
Idle wait for background work after the final turn10 minRunning work is stopped and its partial result droppedCLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS (0 = no cap)
Monitor watch5 minWatch times outHeadless docs
MCP server startup with --mcp-config30 sRun starts without the pending serverMCP_TIMEOUT
API request10 minRequest failsAPI_TIMEOUT_MS

Some details matter here:

Credentials expire too

Login expiry is not a timeout setting, but on a schedule it has the same effect. Once a /login session expires, each model request fails with Login expired. For scripts, claude setup-token creates a one-year OAuth token that you set as CLAUDE_CODE_OAUTH_TOKEN. However, bare mode does not read CLAUDE_CODE_OAUTH_TOKEN. With --bare, use ANTHROPIC_API_KEY or an apiKeyHelper instead. Also note that in -p mode, a set ANTHROPIC_API_KEY is always used when present. An old key left in your shell environment will therefore override your subscription login.

Trigger 3: Permission prompts with no one to answer

This trigger causes the most confusion. In an interactive session, Claude asks and waits for you. In a headless run, what happens depends on whether there is a "permission host":

For unattended jobs, pass --permission-prompts none (v2.1.259 or later). Anything that would prompt is denied unless a PermissionRequest hook allows it, and Claude is told not to retry.

Set the permission mode explicitly

If nothing sets a permission mode, the run uses the built-in starting mode, which can be auto. Choose the mode yourself:

ModeRuns without askingGood fit for headless
dontAskPre-approved tools, reads in working dirs, read-only commands; everything else deniedLocked-down recurring jobs
acceptEditsFile edits plus mkdir, touch, mv, cp; other shell commands still need allow rulesLint or formatting fixers
autoA classifier reviews each action (source)Broader tasks where you still want guardrails
bypassPermissionsAll tools except explicit ask rules and similar exceptions. Can't run as root on Unix; only for isolated environmentsThrowaway containers only

Allow-rule mistakes that lead to denials

One more thing: without --bare, a -p run executes the hooks in the project's .claude/settings.json and connects its .mcp.json servers, even in an untrusted folder. That can change how a job behaves from one machine to another. Anthropic recommends --bare for scripted calls and says it will become the default for -p.

A prevention checklist

Here is an example nightly job that applies the points above. Adjust the paths, tools and numbers for your own job.

#!/usr/bin/env bash
# Example: nightly dependency check (illustrative values)
export BASH_DEFAULT_TIMEOUT_MS=600000     # long test suites
export CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=900000

claude --bare -p "Update dependency pins and run the tests" \
  --append-system-prompt-file ./agent-rules.md \
  --permission-mode dontAsk \
  --permission-prompts none \
  --allowedTools "Read,Edit,Bash(npm test *),Bash(npm install *),Bash(git diff *)" \
  --max-turns 40 \
  --max-budget-usd 3.00 \
  --output-format json > run.json
code=$?

subtype=$(jq -r '.subtype' run.json)
[ "$code" -ne 0 ] || [ "$subtype" != "success" ] && echo "Agent stopped: $subtype (exit $code)" >&2

Go through this list before you schedule a job:

Budgets

Timeouts

Permissions

When to raise a limit and when to narrow the task

If a job keeps hitting error_max_turns or error_max_budget_usd, raising the limit is not always the answer. A reasonable rule: if the turns are spent on useful work and the result is close to done, raise the limit. If the transcript shows repeated retries, denied tools or the same files being read again and again, narrow the task instead. Split it into smaller runs, hand subtasks to subagents (which start with a fresh context and only return a summary), or lower effort for routine steps. Shorter jobs with fixed limits and checked results are easier to run without supervision than one long open-ended job.

Sources