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

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:
- The check uses a client-side cost estimate, which can differ from your bill.
- Spend can go past the cap. The response that reaches it still finishes, and running subagents keep spending until they stop. Anthropic says to leave headroom.
- Subagent spend counts toward the cap. After the cap is reached, starting another subagent fails with
Budget limit reached, and background subagents are stopped (source). - When you continue a run with
--continueor--resume, the totals from earlier runs don't count toward the new cap.
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:
| Timer | Default | What happens | Setting |
|---|---|---|---|
| Foreground Bash command | 2 min | Command times out | BASH_DEFAULT_TIMEOUT_MS |
| Max Bash timeout Claude can request | 10 min | Ceiling for long commands | BASH_MAX_TIMEOUT_MS |
| Idle wait for background work after the final turn | 10 min | Running work is stopped and its partial result dropped | CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS (0 = no cap) |
| Monitor watch | 5 min | Watch times out | Headless docs |
MCP server startup with --mcp-config | 30 s | Run starts without the pending server | MCP_TIMEOUT |
| API request | 10 min | Request fails | API_TIMEOUT_MS |
Some details matter here:
- Bare mode: with
--bare, no background tasks run, so a command that reaches its timeout stops instead of moving to the background. This affects long test suites or builds. - Missing MCP servers don't fail the run. Claude Code skips invalid
--mcp-configentries and the run continues and exits cleanly. When stderr is captured (as in CI), it prints no warning. The skipped entries only appear inmcp_server_errorsin thesystem/initevent. A CI gate can check whether that array is empty. - Your own scheduler can stop the run. If a CI runner or process supervisor sends SIGTERM, Claude Code exits with code 143 and records no result for the unfinished turn. To end the turn cleanly, send SIGINT first. To continue the interrupted turn when you resume, set
CLAUDE_CODE_RESUME_INTERRUPTED_TURN=1(source). - Large piped input fails. Piped stdin is capped at 10MB. For bigger inputs, write them to a file and put the file path in the prompt.
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":
- No host (plain
claude -p): any request that would show a prompt is denied. Claude gets a rejection message and usually tries something else or reports that it couldn't continue. From outside, the job looks like it quit early. - With a host (an SDK
canUseToolcallback or--permission-prompt-tool): the run waits for that host to answer each request. If the host is not answering, the run can wait until a timer ends it.
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:
| Mode | Runs without asking | Good fit for headless |
|---|---|---|
dontAsk | Pre-approved tools, reads in working dirs, read-only commands; everything else denied | Locked-down recurring jobs |
acceptEdits | File edits plus mkdir, touch, mv, cp; other shell commands still need allow rules | Lint or formatting fixers |
auto | A classifier reviews each action (source) | Broader tasks where you still want guardrails |
bypassPermissions | All tools except explicit ask rules and similar exceptions. Can't run as root on Unix; only for isolated environments | Throwaway containers only |
Allow-rule mistakes that lead to denials
- **Missing space before
*.**Bash(git diff )matches commands starting withgit diff. Without the space,Bash(git diff)would also matchgit diff-index. - Bare
Bashin auto mode. In a run that starts in auto mode, a bareBashentry is dropped and the classifier evaluates each command instead. - Tools that
dontAskalways denies.AskUserQuestion, MCP tools markedrequiresUserInteractionand reads from network paths are denied even when an allow rule matches. If your prompt tells Claude to "ask me if unsure", that question will be denied in a headless run.
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
- [ ]
--max-turnsis set with headroom, anderror_max_turnstriggers a resume or an alert. - [ ]
--max-budget-usdis set a bit below your real limit, since spend can go past it. - [ ] Permanent rules live in CLAUDE.md or
--append-system-prompt-file, not only in the opening prompt, so compaction doesn't drop them. - [ ] Noisy commands are piped through
tailorgrepso they don't fill the context window.
Timeouts
- [ ]
BASH_DEFAULT_TIMEOUT_MS/BASH_MAX_TIMEOUT_MSare longer than your slowest build or test command. - [ ]
CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MSfits jobs that use background subagents. - [ ] The CI or job timeout is longer than the agent's own limits, and it sends SIGINT before SIGTERM.
- [ ] Your gate fails when
mcp_server_errorsis not empty. - [ ] Authentication matches the mode: API key or
apiKeyHelperwith--bare,CLAUDE_CODE_OAUTH_TOKENwithout it. You have a reminder before the one-year token expires.
Permissions
- [ ] The permission mode is set explicitly (
dontAskis a sensible default). - [ ]
--permission-prompts noneis set so nothing waits on a host. - [ ] Every command the task needs is in
--allowedTools, with correct*spacing. - [ ] The prompt never asks Claude to check with a human.
- [ ] The script logs
permission_denialsafter each run, and you add allow rules (or narrow the task) based on what it shows.
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.