<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Unattended</title><link>https://en.aobaworks.com/</link><atom:link href="https://en.aobaworks.com/feed-full.xml" rel="self" type="application/rss+xml"/><description>Claude Code tutorials and AI agent automation guides: hooks, headless mode, scheduling, permissions and cost control.</description><language>en</language><lastBuildDate>Mon, 19 Oct 2026 03:00:00 GMT</lastBuildDate><copyright>© 2026 Aoba Works</copyright><item><title>AI Agent Guardrails Example: Copy-Paste Claude Code Rules</title><link>https://en.aobaworks.com/ai-agent-guardrails-example-copy-paste-claude-code-rules/</link><guid isPermaLink="true">https://en.aobaworks.com/ai-agent-guardrails-example-copy-paste-claude-code-rules/</guid><pubDate>Mon, 19 Oct 2026 03:00:00 GMT</pubDate><dc:creator>Aoba Works</dc:creator><description>An AI agent guardrails example for Claude Code: copy-paste permission rules, hooks and CLI caps that stop runaway loops, destructive commands and cost spikes.</description><category>claude code</category><category>ai agents</category><category>guardrails</category><category>hooks</category><category>permissions</category><category>automation</category><content:encoded><![CDATA[<figure><img src="https://en.aobaworks.com/og/ai-agent-guardrails-example-copy-paste-claude-code-rules.png" alt="AI Agent Guardrails Example: Copy-Paste Claude Code Rules" width="1200" height="630"></figure><p class="meta ai-note">How this guide was made: drafted with AI assistance from the official sources linked below, then passed an automated editorial check before publishing. <a href="https://en.aobaworks.com/about/#how-guides-are-made">Our process</a></p><p>Here is a working AI agent guardrails example for Claude Code. Use deny rules in <code>.claude/settings.json</code> to block dangerous tools, a <code>PreToolUse</code> hook that exits with code 2 to catch the destructive commands those rules miss, and the <code>--max-turns</code> and <code>--max-budget-usd</code> flags to cap loops and spending. Each layer covers a different failure. Together they let a scheduled run work without someone watching it.</p>
<p>Claude Code's hooks give you, in Anthropic's words, "<a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener">deterministic control: certain actions always happen rather than relying on the LLM to choose to run them</a>." So a guardrail written as a hook doesn't depend on the model remembering an instruction.</p>
<h2 id="which-option-is-right-for-you">Which option is right for you?</h2>
<p>The right first guardrail depends on how you run Claude Code today. Start with the row that fits you and add the others later.</p>
<div class="table-wrap"><table><thead><tr><th>Your situation</th><th>What to set up first</th><th>First page to open</th></tr></thead><tbody><tr><td>Beginner, interactive sessions only</td><td>Deny rules in <code>.claude/settings.json</code></td><td><a href="https://code.claude.com/docs/en/permissions" rel="noopener">Configure permissions</a></td></tr><tr><td>Daily work, cron or CI jobs</td><td><code>--permission-mode dontAsk</code> plus <code>--max-turns</code></td><td><a href="https://code.claude.com/docs/en/headless" rel="noopener">Run Claude Code programmatically</a></td></tr><tr><td>You've had a destructive command slip through</td><td>A <code>PreToolUse</code> blocking hook</td><td><a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a></td></tr><tr><td>Tight budget, API-key billing</td><td><code>--max-budget-usd</code> plus a Console workspace spend limit</td><td><a href="https://code.claude.com/docs/en/costs" rel="noopener">Manage costs</a></td></tr></tbody></table></div>
<p>Why each pick: deny rules cost nothing and apply to every session. <code>dontAsk</code> keeps unattended runs from stalling. Hooks can match patterns that rules can't. Budget caps stop the one failure you otherwise find out about from your invoice.</p>
<h2 id="what-does-an-ai-agent-guardrail-look-like-in-claude-code">What does an AI agent guardrail look like in Claude Code?</h2>
<p>In Claude Code, a guardrail is a permission rule, hook script or CLI cap that limits the agent's actions, runtime and spending.</p>
<p>Most guardrail explainers you'll find for this query sort guardrails into input, output and system-level types, then use chatbot examples like PII redaction or bank transfer approvals. That framing works for customer-facing bots. A coding agent running at 3 a.m. fails in other ways, so this guide is organized around three of them:</p>
<div class="table-wrap"><table><thead><tr><th>Failure scenario</th><th>What it looks like</th><th>Guardrail layer</th></tr></thead><tbody><tr><td>Destructive command</td><td><code>rm -rf</code>, <code>git push --force</code>, <code>git reset --hard</code></td><td>Deny rules + <code>PreToolUse</code> hook</td></tr><tr><td>Runaway loop</td><td>The agent keeps retrying a failing fix for hours</td><td><code>--max-turns</code> + tool-call counter hook</td></tr><tr><td>Cost spike</td><td>One overnight job burns a week of budget</td><td><code>--max-budget-usd</code> + spend limits</td></tr><tr><td>Premature "done"</td><td>The agent stops while tests still fail</td><td><code>Stop</code> hook</td></tr></tbody></table></div>
<p>If you haven't set up hooks before, read <a href="https://en.aobaworks.com/claude-code-hooks-lifecycle-every-event-explained/">Claude Code Hooks Lifecycle: Every Event, Explained</a> first. It shows where <code>PreToolUse</code> and <code>Stop</code> fire.</p>
<h2 id="how-do-you-block-destructive-commands-with-permission-rules">How do you block destructive commands with permission rules?</h2>
<p>Claude Code permission rules go in <code>permissions.allow</code>, <code>ask</code> and <code>deny</code> arrays in <code>.claude/settings.json</code>. Deny rules block matching tool calls before they run.</p>
<h3 id="step-1-create-the-project-settings-file">Step 1: Create the project settings file</h3>
<ol><li>In your repo root, create <code>.claude/settings.json</code> if it doesn't exist.</li><li>Paste this starting point and change the commands to match your stack:</li></ol>
<pre data-lang="json"><code class="language-json">{
  &quot;permissions&quot;: {
    &quot;allow&quot;: [
      &quot;Bash(npm run test *)&quot;,
      &quot;Bash(npm run lint *)&quot;,
      &quot;Bash(git status *)&quot;,
      &quot;Bash(git diff *)&quot;,
      &quot;Bash(git commit *)&quot;
    ],
    &quot;ask&quot;: [
      &quot;Bash(git push *)&quot;
    ],
    &quot;deny&quot;: [
      &quot;Bash(rm -rf *)&quot;,
      &quot;Bash(git push --force *)&quot;,
      &quot;Bash(git reset --hard *)&quot;,
      &quot;Read(./.env)&quot;,
      &quot;Read(./.env.*)&quot;,
      &quot;Edit(./.env)&quot;
    ]
  }
}</code></pre>
<ol><li>Commit the file so every machine and every scheduled job uses the same rules. The <a href="https://code.claude.com/docs/en/permissions" rel="noopener">permissions docs</a> note that you "can check permission settings into version control to share them."</li></ol>
<h3 id="step-2-get-the-wildcard-right">Step 2: Get the wildcard right</h3>
<p>The trailing <code> <em></code> turns a rule into a prefix match. The <a href="https://code.claude.com/docs/en/headless" rel="noopener">headless docs</a> warn that "the space before <code></em></code> is important": <code>Bash(git diff*)</code> would also match <code>git diff-index</code>.</p>
<h3 id="common-mistake-trusting-a-scoped-deny-rule-to-catch-every-variant">Common mistake: trusting a scoped deny rule to catch every variant</h3>
<p>The <a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference</a> says a scoped rule like <code>Bash(rm <em>)</code> "denies only calls that match as written." For example, <code>Bash(rm -rf </em>)</code> won't match <code>rm -r -f build/</code> or <code>find . -delete</code>. The fix is the hook in the next section, which reads the actual command string and can match several patterns.</p>
<p>Also notice the <code>ask</code> rule for <code>git push</code>. In a <code>-p</code> run with no permission host, the headless docs say requests that would prompt "are denied either way". So in unattended runs, <code>ask</code> effectively means "never". That's usually what you want for pushes.</p>
<h2 id="how-do-you-write-a-hook-that-stops-dangerous-commands">How do you write a hook that stops dangerous commands?</h2>
<p>A Claude Code <code>PreToolUse</code> hook runs before every matching tool call. If the script exits with code 2, the call is blocked. The <a href="https://code.claude.com/docs/en/hooks" rel="noopener">hooks reference</a> puts it this way: "Exit code 2 is the way a hook signals 'stop, don't do this'."</p>
<h3 id="step-1-save-the-script">Step 1: Save the script</h3>
<p>Save this as <code>.claude/hooks/block-destructive.sh</code> and run <code>chmod +x</code> on it. It needs <code>jq</code> installed.</p>
<pre data-lang="bash"><code class="language-bash">#!/bin/bash
# Example guardrail: block destructive shell commands
cmd=$(jq -r &#x27;.tool_input.command // empty&#x27;)

if echo &quot;$cmd&quot; | grep -Eiq \
  &#x27;rm +-[a-z]*r[a-z]* *-?[a-z]*f|rm +-r +-f|find .* -delete|git +push .*(--force|-f)|git +reset +--hard|git +clean +-[a-z]*f|drop +(table|database)|terraform +destroy&#x27;; then
  echo &quot;Blocked by guardrail: &#x27;$cmd&#x27; matches a destructive pattern. Use a safer alternative or leave a note for a human.&quot; &gt;&amp;2
  exit 2
fi
exit 0</code></pre>
<h3 id="step-2-register-it">Step 2: Register it</h3>
<p>Add a <code>hooks</code> block next to <code>permissions</code> in the same settings file. This follows the structure in the <a href="https://code.claude.com/docs/en/hooks" rel="noopener">hooks reference</a>:</p>
<pre data-lang="json"><code class="language-json">{
  &quot;hooks&quot;: {
    &quot;PreToolUse&quot;: [
      {
        &quot;matcher&quot;: &quot;Bash&quot;,
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh&quot;
          }
        ]
      }
    ]
  }
}</code></pre>
<h3 id="step-3-verify">Step 3: Verify</h3>
<ol><li>Start <code>claude</code> in the repo and type <code>/hooks</code>.</li><li>Check that your script appears under <strong>PreToolUse</strong>. The <a href="https://code.claude.com/docs/en/costs" rel="noopener">costs docs</a> describe the same check for their example hook.</li><li>Ask Claude to run <code>rm -r -f tmp/</code> on a throwaway folder. The call should come back blocked.</li></ol>
<p>Common mistakes: the script isn't executable (<code>chmod +x</code> fixes it), <code>jq</code> is missing on the cron host, or the matcher is <code>bash</code> in lowercase. Matchers are tested as JavaScript regular expressions, so case matters (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">hooks reference</a>). For six more scripts in the same style, see <a href="https://en.aobaworks.com/claude-code-hooks-examples-7-copy-paste-scripts/">Claude Code Hooks Examples: 7 Copy-Paste Scripts</a>.</p>
<h2 id="how-do-you-stop-runaway-loops-in-unattended-runs">How do you stop runaway loops in unattended runs?</h2>
<p>Claude Code's <code>--max-turns</code> flag caps agentic turns in print mode. The <a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference</a> is clear that there is "No limit by default." Set it on every scheduled job.</p>
<h3 id="the-cron-command">The cron command</h3>
<p>Here is a cron-safe invocation that combines the caps:</p>
<pre data-lang="bash"><code class="language-bash">claude -p &quot;Run the test suite, fix failing tests in src/, and commit the fix&quot; \
  --permission-mode dontAsk \
  --permission-prompts none \
  --max-turns 30 \
  --max-budget-usd 3.00 \
  --output-format json</code></pre>
<p>What each flag does, per the official docs:</p>
<ul><li><code>--permission-mode dontAsk</code>: "Claude Code denies every call that would otherwise prompt, which is useful for locked-down CI runs" (<a href="https://code.claude.com/docs/en/headless" rel="noopener">headless docs</a>). Your <code>allow</code> rules still apply.</li><li><code>--permission-prompts none</code>: Claude is told that nobody can approve a request and not to retry it, so it doesn't burn turns asking again. This needs v2.1.259 or later (<a href="https://code.claude.com/docs/en/headless" rel="noopener">headless docs</a>).</li><li><code>--max-turns 30</code>: the run "exits with an error when the limit is reached" (<a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference</a>). Your script can branch on the non-zero exit code.</li></ul>
<p>The number 30 is an example. Pick it by looking at how many turns a normal successful run takes and adding a margin.</p>
<h3 id="a-second-layer-count-tool-calls">A second layer: count tool calls</h3>
<p>Turns and tool calls are different things, so a hook that counts tool calls per session gives you a backstop. This example uses <code>session_id</code>, which every hook receives in its input:</p>
<pre data-lang="bash"><code class="language-bash">#!/bin/bash
# Example: deny tool calls after 150 in one session
input=$(cat)
sid=$(echo &quot;$input&quot; | jq -r &#x27;.session_id&#x27;)
f=&quot;/tmp/claude-toolcalls-$sid&quot;
n=$(( $(cat &quot;$f&quot; 2&gt;/dev/null || echo 0) + 1 ))
echo &quot;$n&quot; &gt; &quot;$f&quot;
if [ &quot;$n&quot; -gt 150 ]; then
  echo &quot;Tool-call limit reached. Stop, summarize progress, and list what is unresolved.&quot; &gt;&amp;2
  exit 2
fi
exit 0</code></pre>
<p>Register it under <code>PreToolUse</code> with the matcher <code>.*</code> so every tool counts. If runs stop before you expect, work through <a href="https://en.aobaworks.com/why-claude-code-stops-mid-automation-and-how-to-fix-it/">Why Claude Code Stops Mid-Automation (and How to Fix It)</a>. Your own guardrail may be the cause.</p>
<h3 id="stop-hook-don-t-let-it-quit-while-tests-fail">Stop hook: don't let it quit while tests fail</h3>
<p>A <code>Stop</code> hook can send Claude back to work by returning <code>{"decision": "block", "reason": "..."}</code> (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">hooks reference</a>). Always check <code>stop_hook_active</code> first, or the hook can bounce Claude back indefinitely:</p>
<pre data-lang="bash"><code class="language-bash">#!/bin/bash
input=$(cat)
[ &quot;$(echo &quot;$input&quot; | jq -r &#x27;.stop_hook_active&#x27;)&quot; = &quot;true&quot; ] &amp;&amp; exit 0
if ! npm test --silent &gt;/dev/null 2&gt;&amp;1; then
  echo &#x27;{&quot;decision&quot;:&quot;block&quot;,&quot;reason&quot;:&quot;Tests still fail. Fix them, or explain why they cannot be fixed, before stopping.&quot;}&#x27;
fi
exit 0</code></pre>
<h2 id="how-do-you-cap-claude-code-costs-per-run">How do you cap Claude Code costs per run?</h2>
<p>Claude Code's <code>--max-budget-usd</code> flag stops a print-mode run once estimated API spend reaches your cap. Treat it as a soft cap, not an exact one.</p>
<p>The <a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference</a> gives two caveats. The cap is checked against a client-side estimate that "can differ from your bill," and "Spend can pass the cap, so leave headroom." Subagent spend counts toward it (<a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference</a>).</p>
<p>For a sense of scale, Anthropic reports that across enterprise deployments the average is "<a href="https://code.claude.com/docs/en/costs" rel="noopener">around $13 per developer per active day</a>". That's an interactive-use average, not a figure for automations, but it's a useful sanity check. A nightly job that costs more than a full day of human use is probably looping.</p>
<p>Practical layers, from cheapest to strongest:</p>
<ol><li>Add <code>--output-format json</code> and log <code>total_cost_usd</code> from each run (<a href="https://code.claude.com/docs/en/headless" rel="noopener">headless docs</a>). Read the log weekly.</li><li>Set <code>--max-budget-usd</code> at roughly twice your typical run cost.</li><li>On API billing, set a workspace spend limit in the Claude Console. The <a href="https://code.claude.com/docs/en/costs" rel="noopener">costs docs</a> list "Workspace spend limits" as the cap for Console users.</li></ol>
<p>To find where tokens go in the first place, see <a href="https://en.aobaworks.com/claude-code-how-to-see-token-usage-and-reduce-it/">Claude Code: How to See Token Usage (and Reduce It)</a>.</p>
<h2 id="which-guardrail-layer-catches-which-failure">Which guardrail layer catches which failure?</h2>
<p>Claude Code's layers overlap on purpose. No single layer covers everything, so pick by failure mode:</p>
<div class="table-wrap"><table><thead><tr><th>Layer</th><th>Catches</th><th>Misses</th><th>Cost to set up</th></tr></thead><tbody><tr><td>Deny rules</td><td>Exact known commands, secret files</td><td>Reworded variants</td><td>5 minutes</td></tr><tr><td><code>PreToolUse</code> hook</td><td>Pattern variants, custom logic</td><td>Anything your regex doesn't cover</td><td>15–30 minutes</td></tr><tr><td><code>--max-turns</code> / tool-call counter</td><td>Loops</td><td>A run that spends a lot in a few turns</td><td>1 minute</td></tr><tr><td><code>--max-budget-usd</code></td><td>Cost spikes</td><td>Overshoot past the cap</td><td>1 minute</td></tr><tr><td><code>Stop</code> hook</td><td>Quitting with failing tests</td><td>Bad code that still passes the tests</td><td>10 minutes</td></tr></tbody></table></div>
<p>One limitation applies to all of them. A <code>-p</code> session runs the hooks in a project's <code>.claude/settings.json</code> "<a href="https://code.claude.com/docs/en/headless" rel="noopener">even in a folder you've never trusted</a>." Only point unattended jobs at repos you control, or use <code>--bare</code> with an explicit <code>--settings</code> file. Also keep a recovery path, because guardrails reduce damage but don't undo it. <a href="https://en.aobaworks.com/claude-code-checkpoints-how-rollback-works-in-automations/">Claude Code Checkpoints: How Rollback Works in Automations</a> covers what you can roll back.</p>
<h2 id="next-steps">Next steps</h2>
<p><strong>Do today (DIY):</strong></p>
<ol><li>Paste the <code>permissions</code> block into <code>.claude/settings.json</code> and commit it.</li><li>Add <code>block-destructive.sh</code>, register it, and confirm it shows up under <code>/hooks</code>.</li><li>Add <code>--max-turns</code> and <code>--max-budget-usd</code> to every scheduled <code>claude -p</code> line. The <a href="https://en.aobaworks.com/claude-code-cron-job-setup-run-automations-overnight/">cron setup guide</a> shows where those lines live.</li></ol>
<p><strong>If you'd rather not build this yourself:</strong> a Claude subscription doesn't configure guardrails for you, but it does put a hard ceiling on usage. Pro is $20/month and Max starts at $100/month, and Claude Code is included in paid plans (as of October 2026; check <a href="https://claude.com/pricing" rel="noopener">claude.com/pricing</a>). A subscription suits solo builders who want predictable monthly spend. It's a poor fit if you need per-run dollar accounting: the costs docs note that the <code>/usage</code> session cost figure "isn't relevant for billing purposes" for Pro and Max subscribers. For per-token control, API billing through the Claude Console with workspace spend limits is the better choice.</p>
<p><strong>Related guides on this site:</strong></p>
<ul><li><a href="https://en.aobaworks.com/claude-code-hooks-examples-7-copy-paste-scripts/">Claude Code Hooks Examples: 7 Copy-Paste Scripts</a></li><li><a href="https://en.aobaworks.com/claude-code-hooks-lifecycle-every-event-explained/">Claude Code Hooks Lifecycle: Every Event, Explained</a></li><li><a href="https://en.aobaworks.com/why-claude-code-stops-mid-automation-and-how-to-fix-it/">Why Claude Code Stops Mid-Automation (and How to Fix It)</a></li></ul>
<h2 id="faq">FAQ</h2>
<h3 id="what-is-an-example-of-an-agent-in-ai">What is an example of an agent in AI?</h3>
<p>Claude Code is an example of an AI agent. It reads files, runs shell commands, edits code and decides its next step from the results, all in a loop. That autonomy is what makes guardrails necessary. A chatbot only produces text, while an agent acts on your system.</p>
<h3 id="is-a-claude-md-instruction-enough-as-a-guardrail">Is a CLAUDE.md instruction enough as a guardrail?</h3>
<p>No. A line like "never force-push" in CLAUDE.md is guidance the model usually follows, not enforcement. Permission rules and hooks are checked by Claude Code itself on every tool call, so use them for anything that would be costly to get wrong.</p>
<h3 id="should-i-use-bypasspermissions-for-overnight-jobs">Should I use bypassPermissions for overnight jobs?</h3>
<p>For most solo setups, no. <code>--dangerously-skip-permissions</code> is equivalent to <code>bypassPermissions</code> (<a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference</a>). <code>dontAsk</code> with explicit allow rules lets routine work through and denies everything else, which is a safer default for runs nobody watches.</p>
<h3 id="do-ask-rules-work-in-cron-runs">Do ask rules work in cron runs?</h3>
<p>Not as prompts. In a <code>-p</code> run with no permission host, anything that would prompt is denied (<a href="https://code.claude.com/docs/en/headless" rel="noopener">headless docs</a>). Treat <code>ask</code> as "deny when unattended, ask when I'm at the keyboard."</p>
<h2 id="sources">Sources</h2>
<ul><li><a href="https://code.claude.com/docs/en/permissions" rel="noopener">Configure permissions – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener">Automate actions with hooks – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/headless" rel="noopener">Run Claude Code programmatically – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/permission-modes" rel="noopener">Choose a permission mode – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/costs" rel="noopener">Manage costs effectively – Claude Code Docs</a></li><li><a href="https://claude.com/pricing" rel="noopener">Claude pricing</a></li><li><a href="https://www.altexsoft.com/blog/ai-guardrails/" rel="noopener">AI Guardrails in Agentic Systems Explained – AltexSoft</a></li><li><a href="https://www.cequence.ai/learn/ai-security/types-of-ai-guardrails-examples-best-practices/" rel="noopener">5 Types of AI Guardrails, Examples, and Implementation Best Practices – Cequence</a></li><li><a href="https://galileo.ai/blog/best-ai-agent-guardrails-solutions" rel="noopener">8 Best AI Agent Guardrails Solutions in 2026 – Galileo</a></li><li><a href="https://arxiv.org/abs/2311.09735" rel="noopener">GEO: Generative Engine Optimization (arXiv:2311.09735)</a></li></ul><p>Originally published at <a href="https://en.aobaworks.com/ai-agent-guardrails-example-copy-paste-claude-code-rules/">Unattended</a>.</p>]]></content:encoded><enclosure url="https://en.aobaworks.com/og/ai-agent-guardrails-example-copy-paste-claude-code-rules.png" type="image/png" length="42219"/><media:content url="https://en.aobaworks.com/og/ai-agent-guardrails-example-copy-paste-claude-code-rules.png" medium="image" type="image/png" width="1200" height="630"/></item><item><title>Claude Code Rate Limit Reached: Retry Setup for Cron Jobs</title><link>https://en.aobaworks.com/claude-code-rate-limit-reached-retry-setup-for-cron-jobs/</link><guid isPermaLink="true">https://en.aobaworks.com/claude-code-rate-limit-reached-retry-setup-for-cron-jobs/</guid><pubDate>Mon, 19 Oct 2026 03:00:00 GMT</pubDate><dc:creator>Aoba Works</dc:creator><description>Claude Code rate limit reached? Tell a 429 throttle from a plan or spend cap, read the retry-after headers, and set safe auto-retries for headless cron jobs.</description><category>claude code</category><category>rate limits</category><category>429</category><category>cron</category><category>automation</category><category>headless</category><content:encoded><![CDATA[<figure><img src="https://en.aobaworks.com/og/claude-code-rate-limit-reached-retry-setup-for-cron-jobs.png" alt="Claude Code Rate Limit Reached: Retry Setup for Cron Jobs" width="1200" height="630"></figure><p class="meta ai-note">How this guide was made: drafted with AI assistance from the official sources linked below, then passed an automated editorial check before publishing. <a href="https://en.aobaworks.com/about/#how-guides-are-made">Our process</a></p><p>"Rate limit reached" in Claude Code means one of three things. You sent too many requests or tokens per minute on the API, you used up your subscription's 5-hour or weekly allowance, or you hit a spend cap. Only the first one goes away if you retry. Anthropic's rule for that case is simple: "When the response has a <code>retry-after</code> header, wait that many seconds before retrying" (<a href="https://platform.claude.com/docs/en/api/rate-limits" rel="noopener">rate limits docs</a>). For unattended jobs, set <code>CLAUDE_CODE_RETRY_WATCHDOG=1</code> and add a wrapper that stops on quota or spend errors instead of looping on them.</p>
<h2 id="which-option-is-right-for-you">Which option is right for you?</h2>
<p>Pick the row that matches how you run Claude Code. Each row says what to do first and which page to open.</p>
<div class="table-wrap"><table><thead><tr><th>Your situation</th><th>What to pick first</th><th>First page to open</th></tr></thead><tbody><tr><td>Beginner on Pro, hit the limit in the terminal</td><td>Wait for the reset time shown, then check <code>/usage</code></td><td><a href="https://code.claude.com/docs/en/errors" rel="noopener">Claude Code errors reference</a></td></tr><tr><td>Daily work use, often out of allowance by afternoon</td><td>Cut context (<code>/clear</code>, Sonnet for routine tasks), then think about Max or usage credits</td><td><a href="https://code.claude.com/docs/en/costs" rel="noopener">Manage costs effectively</a></td></tr><tr><td>Nightly cron or CI job on an API key</td><td>Turn on <code>CLAUDE_CODE_RETRY_WATCHDOG=1</code> and cap the wait time</td><td><a href="https://code.claude.com/docs/en/errors" rel="noopener">Errors: automatic retries</a></td></tr><tr><td>Tight budget, API billing</td><td>Set your own spend limit below your tier cap</td><td><a href="https://platform.claude.com/settings/billing" rel="noopener">Console Billing page</a></td></tr><tr><td>Shared org key, Claude Code starving production apps</td><td>Set a workspace rate limit on the "Claude Code" workspace</td><td><a href="https://platform.claude.com/usage/limits" rel="noopener">Console Rate limits page</a></td></tr></tbody></table></div>
<p>Why these picks: retrying a subscription limit or a spend cap won't work, so beginners should read the reset time first. Cron users need retries that stop on their own. Budget users get more out of a hard cap than a bigger tier.</p>
<h2 id="which-claude-code-rate-limit-did-you-actually-hit">Which Claude Code rate limit did you actually hit?</h2>
<p>You can tell which limit you hit from the wording of the Claude Code message. Each wording has a different cause, and retrying only helps for some of them.</p>
<p>The <a href="https://code.claude.com/docs/en/errors" rel="noopener">Claude Code errors reference</a> lists these message families:</p>
<div class="table-wrap"><table><thead><tr><th>Message you see</th><th>What it is</th><th>Will retrying help?</th></tr></thead><tbody><tr><td><code>You've hit your session limit · resets 3:45pm</code> / <code>weekly limit</code></td><td>Subscription allowance used up</td><td>No. Wait for the reset or add usage credits</td></tr><tr><td><code>You've hit your Opus limit</code> / <code>Sonnet limit</code></td><td>Limit for one model family</td><td>Switch family with <code>/model</code></td></tr><tr><td><code>Request rejected (429)</code></td><td>API throttle (RPM/ITPM/OTPM) or a spend-limit 429</td><td>Throttle: yes, it retries itself. Spend limit: no</td></tr><tr><td><code>Server is temporarily limiting requests</code></td><td>Server-side capacity throttle, not your quota</td><td>Yes, after a short wait</td></tr><tr><td><code>Repeated 529 Overloaded errors</code></td><td>API at capacity for everyone</td><td>Yes. Also check <a href="https://status.claude.com" rel="noopener">status.claude.com</a></td></tr><tr><td><code>You've hit your monthly spend limit</code> (and variants)</td><td>A spend cap set by you or your org</td><td>No. Raise the cap</td></tr><tr><td><code>Credit balance is too low</code></td><td>Prepaid API credits ran out</td><td>No. Add credits</td></tr></tbody></table></div>
<p>At the API level, a 429 has the type <code>rate_limit_error</code>. It covers three cases: a rate limit, your usage tier's monthly spend cap, or a spend limit on the Claude Code workspace (<a href="https://platform.claude.com/docs/en/api/errors" rel="noopener">API errors</a>). A 529 <code>overloaded_error</code> is different. The errors reference says a 529 does not count against your quota.</p>
<p>The tier spend cap is the one that catches automations off guard. It comes back as a 429 with <code>error_code: enforced_spend_limit_reached</code> and no <code>retry-after</code> header. The docs warn that "Retrying, including the SDK's automatic retries, fails until access resumes" (<a href="https://platform.claude.com/docs/en/api/rate-limits" rel="noopener">rate limits</a>).</p>
<h2 id="how-do-claude-code-limits-differ-by-plan-and-api-tier">How do Claude Code limits differ by plan and API tier?</h2>
<p>Claude Code subscriptions use rolling 5-hour and weekly allowances. API keys use per-minute token buckets plus a monthly spend cap.</p>
<h3 id="subscription-plans-pro-max-team-enterprise">Subscription plans (Pro, Max, Team, Enterprise)</h3>
<p>On Team and Enterprise, each member's usage "draws from a per-seat allowance that resets on a rolling five-hour window and a weekly window" (<a href="https://code.claude.com/docs/en/costs" rel="noopener">costs docs</a>). That allowance is shared with Claude chat and Cowork. The same page says session and weekly limits are shared across all models, so switching models with <code>/model</code> only helps after an Opus- or Sonnet-specific message.</p>
<p>Prices (as of October 2026; check the <a href="https://claude.com/pricing" rel="noopener">official pricing page</a>): Pro is $20/month, or $17/month billed annually. Max starts at $100/month. Team Standard seats are $25/month billed monthly, and Premium seats are $125/month and give "5x more usage than standard seats." The Free plan doesn't include Claude Code.</p>
<p>When you run out, <code>/usage-credits</code> lets Pro and Max subscribers keep going on <a href="https://support.claude.com/en/articles/12429409-extra-usage-for-paid-claude-plans" rel="noopener">usage credits</a>. It doesn't work with API key authentication (<a href="https://code.claude.com/docs/en/costs" rel="noopener">costs docs</a>).</p>
<h3 id="api-tiers-claude-console">API tiers (Claude Console)</h3>
<p>API limits are counted in requests per minute (RPM), input tokens per minute (ITPM) and output tokens per minute (OTPM), separately for each model class. They are enforced with a token bucket that refills continuously (<a href="https://platform.claude.com/docs/en/api/rate-limits" rel="noopener">rate limits</a>):</p>
<div class="table-wrap"><table><thead><tr><th>Tier</th><th>Monthly spend cap</th><th>Sonnet 5 / Opus 5.5 RPM</th><th>ITPM</th><th>OTPM</th></tr></thead><tbody><tr><td>Start</td><td>$500</td><td>1,000</td><td>2,000,000</td><td>400,000</td></tr><tr><td>Build</td><td>$1,000</td><td>5,000</td><td>5,000,000</td><td>1,000,000</td></tr><tr><td>Scale</td><td>$200,000</td><td>10,000</td><td>10,000,000</td><td>2,000,000</td></tr></tbody></table></div>
<p>Three details matter for agents:</p>
<ul><li>Minute limits can be enforced over shorter intervals. The docs say "Short bursts of requests can exceed the limit and trigger rate limit errors." Five parallel subagents starting at once can trip a limit that a per-minute average says you're under.</li><li>For most models, cached input tokens don't count toward ITPM. A good prompt cache hit rate gives you more effective throughput.</li><li>New organizations may start in an Evaluation tier, with limits below those in the table.</li></ul>
<p>If your team shares one org, Anthropic's <a href="https://code.claude.com/docs/en/costs" rel="noopener">costs page</a> suggests 200k–300k TPM and 5–7 RPM per user for teams of 1–5 people.</p>
<h2 id="how-do-you-read-claude-code-s-rate-limit-headers">How do you read Claude Code's rate-limit headers?</h2>
<p>Every API response includes <code>anthropic-ratelimit-*</code> headers that show your limit, how much is left and when it refills. A 429 caused by throttling also carries <code>retry-after</code>.</p>
<p>The headers you'll check most often (<a href="https://platform.claude.com/docs/en/api/rate-limits" rel="noopener">rate limits</a>):</p>
<ul><li><code>retry-after</code>: seconds to wait. Earlier retries will fail.</li><li><code>anthropic-ratelimit-requests-remaining</code> and <code>-requests-reset</code> (an RFC 3339 timestamp)</li><li><code>anthropic-ratelimit-input-tokens-remaining</code> / <code>-output-tokens-remaining</code> (rounded to the nearest thousand)</li><li><code>anthropic-ratelimit-tokens-*</code>: values for whichever limit is tightest right now, including a workspace limit</li></ul>
<p>To see them for your own key, print only the headers. This follows the pattern in the <a href="https://platform.claude.com/docs/en/api/errors" rel="noopener">API errors page</a>:</p>
<pre data-lang="bash"><code class="language-bash">curl -sS -D - -o /dev/null https://api.anthropic.com/v1/messages \
  -H &quot;x-api-key: $ANTHROPIC_API_KEY&quot; \
  -H &quot;anthropic-version: 2023-06-01&quot; \
  -H &quot;content-type: application/json&quot; \
  -d &#x27;{&quot;model&quot;:&quot;claude-sonnet-5&quot;,&quot;max_tokens&quot;:16,&quot;messages&quot;:[{&quot;role&quot;:&quot;user&quot;,&quot;content&quot;:&quot;ping&quot;}]}&#x27; \
  | grep -iE &#x27;retry-after|anthropic-ratelimit&#x27;</code></pre>
<p>Use this rule: a 429 with <code>retry-after</code> is a throttle, so wait that long. A 429 without it, plus <code>enforced_spend_limit_reached</code>, is a spend cap, so stop.</p>
<h2 id="how-do-you-auto-retry-429s-in-a-headless-claude-code-cron-job">How do you auto-retry 429s in a headless Claude Code cron job?</h2>
<p>Claude Code already retries transient failures 10 times with exponential backoff. For cron jobs, add the retry watchdog and a wrapper that exits cleanly when it hits a quota or spend error.</p>
<p>The relevant settings, from the <a href="https://code.claude.com/docs/en/errors" rel="noopener">errors reference</a>:</p>
<div class="table-wrap"><table><thead><tr><th>Variable</th><th>Default</th><th>What it does</th></tr></thead><tbody><tr><td><code>CLAUDE_CODE_MAX_RETRIES</code></td><td>10</td><td>Retry count (capped at 15 unless the watchdog is on)</td></tr><tr><td><code>CLAUDE_CODE_RETRY_WATCHDOG</code></td><td>unset</td><td><code>1</code> = retry 429 and 529 errors indefinitely in unattended runs. Fails at once on spend-limit or exhausted-credit 429s</td></tr><tr><td><code>CLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS</code></td><td>unset (no limit)</td><td>Caps how long each request waits out 429/529 errors (v2.1.295+)</td></tr><tr><td><code>API_TIMEOUT_MS</code></td><td>600000</td><td>Per-request timeout</td></tr></tbody></table></div>
<h3 id="steps">Steps</h3>
<ol><li>Run <code>claude --version</code> and update if you're below v2.1.295. Older versions ignore the wait cap.</li><li>Open your job script (see /claude-code-cron-job-setup-run-automations-overnight/ for the crontab side).</li><li>Export <code>CLAUDE_CODE_RETRY_WATCHDOG=1</code> and set a wait cap, so one stuck request can't push the job into working hours.</li><li>Run with <code>-p --output-format json</code> and save stdout to a log. The <a href="https://code.claude.com/docs/en/headless" rel="noopener">headless docs</a> say Claude Code "exits with code 0 on success and a non-zero code when the run fails," and that failures inside the run print on stdout.</li><li>On a non-zero exit, check the log for quota or spend wording before you rerun anything.</li></ol>
<p>Here is an example wrapper. Adjust the paths and the budget to fit your job:</p>
<pre data-lang="bash"><code class="language-bash">#!/usr/bin/env bash
set -u
export CLAUDE_CODE_RETRY_WATCHDOG=1
export CLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS=1800000   # 30 min per request

LOG=&quot;$HOME/logs/nightly-$(date +%F).json&quot;
claude -p &quot;$(cat &quot;$HOME/jobs/nightly.md&quot;)&quot; \
  --output-format json \
  --max-budget-usd 5 \
  --permission-prompts none &gt; &quot;$LOG&quot;
status=$?

if [ &quot;$status&quot; -ne 0 ]; then
  if grep -qiE &quot;spend limit|session limit|weekly limit|credit balance&quot; &quot;$LOG&quot;; then
    echo &quot;$(date) quota/spend stop, not retrying&quot; &gt;&gt; &quot;$HOME/logs/claude-alerts.log&quot;
    exit 2
  fi
  echo &quot;$(date) failed with $status, see $LOG&quot; &gt;&gt; &quot;$HOME/logs/claude-alerts.log&quot;
  exit &quot;$status&quot;
fi</code></pre>
<p><code>--permission-prompts</code> requires v2.1.259 or later. If you want live retry progress, use <code>--output-format stream-json --verbose</code> and watch for <code>system/api_retry</code> events. Each one includes <code>attempt</code>, <code>max_retries</code>, <code>retry_delay_ms</code>, <code>error_status</code> and an <code>error</code> category such as <code>rate_limit</code> or <code>overloaded</code> (<a href="https://code.claude.com/docs/en/headless" rel="noopener">headless docs</a>).</p>
<p>If you'd rather use a hook to log or alert on failures, see /claude-code-hooks-examples-7-copy-paste-scripts/.</p>
<p>Limitation: the wait-and-continue-after-reset feature (v2.1.234+, set with <code>autoContinueAtUsageLimit</code>) is for open interactive sessions on a claude.ai subscription (<a href="https://code.claude.com/docs/en/errors" rel="noopener">errors</a>, <a href="https://code.claude.com/docs/en/costs" rel="noopener">costs</a>). In a <code>-p</code> cron run, the job ends when it hits a session limit. Schedule the next run after the reset time instead.</p>
<h2 id="common-mistakes-and-fixes">Common mistakes and fixes</h2>
<p>Most repeat rate-limit failures come from retrying something that can't succeed, or from sending bursts.</p>
<ul><li><strong>Retrying a spend cap in a loop.</strong> Your wrapper keeps firing and nothing gets through. Fix: check for <code>enforced_spend_limit_reached</code> or "spend limit" and exit. At the tier cap, access returns at 00:00 UTC on the first of the next month, or when you request a higher tier (<a href="https://platform.claude.com/docs/en/api/rate-limits" rel="noopener">rate limits</a>).</li><li><strong>Expecting a self-set spend limit to return a 429.</strong> It returns HTTP 400 <code>invalid_request_error</code>, except on the Claude Code workspace, which can return a 429 with <code>retry-after</code> (<a href="https://platform.claude.com/docs/en/api/rate-limits" rel="noopener">rate limits</a>).</li><li><strong>Starting five jobs at the same minute.</strong> Bursts trip the per-second enforcement. Fix: spread the crontab entries out (<code><em>/7</code>, <code></em>/11</code>) or run jobs one after another.</li><li><strong>Using <code>--bare</code> with a subscription.</strong> Bare mode never reads OAuth credentials and needs <code>ANTHROPIC_API_KEY</code> (<a href="https://code.claude.com/docs/en/headless" rel="noopener">headless docs</a>).</li><li><strong>Leaving one session open all day.</strong> Each request carries the whole conversation. Use <code>/clear</code> between tasks (<a href="https://code.claude.com/docs/en/costs" rel="noopener">costs</a>). /claude-code-how-to-see-token-usage-and-reduce-it/ covers this in detail.</li></ul>
<h2 id="why-does-claude-code-in-vs-code-say-api-error-rate-limit-reached">Why does Claude Code in VS Code say "API Error: Rate limit reached"?</h2>
<p>The Claude Code VS Code extension runs on the same Claude Code installation and sign-in as the CLI (<a href="https://code.claude.com/docs/en/vs-code" rel="noopener">VS Code docs</a>), and its usage is tracked in the same allowance (<a href="https://code.claude.com/docs/en/costs" rel="noopener">costs docs</a>), so the causes and fixes above apply.</p>
<p>In the extension, open the <strong>Account &amp; usage</strong> dialog and use the <strong>Day</strong> / <strong>Week</strong> toggle to see what used up your allowance (<a href="https://code.claude.com/docs/en/costs" rel="noopener">costs docs</a>). If usage looks low but the error keeps coming back on an API key, check the Console's <a href="https://platform.claude.com/usage/limits" rel="noopener">Rate limits page</a>. A workspace limit set on the "Claude Code" workspace caps Claude Code's share of the org's rate limits, even when the org as a whole has room.</p>
<h2 id="next-steps">Next steps</h2>
<p>Start with the DIY steps below, and only pay for something if they don't solve it.</p>
<p><strong>Do today:</strong></p>
<ol><li>Run <code>/usage</code> and note which bar is closest to full and when it resets.</li><li>Add <code>CLAUDE_CODE_RETRY_WATCHDOG=1</code> plus a <code>CLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS</code> cap to your cron script, and add the spend-limit check.</li><li>API users: set your own limit under <strong>Billing → Spend limits → Adjust limit</strong> in the <a href="https://platform.claude.com/settings/billing" rel="noopener">Console</a>.</li></ol>
<p><strong>If you'd rather not manage this yourself:</strong> the main official option is more capacity. Claude Max starts at $100/month (as of October 2026; check <a href="https://claude.com/pricing" rel="noopener">claude.com/pricing</a>). It suits one person who hits Pro limits on most workdays. It's a poor fit for jobs that need a guaranteed nightly throughput, because the weekly window still applies. For those, an API key on a higher tier (requested via <strong>Request tier increase</strong> on the <a href="https://platform.claude.com/usage/limits" rel="noopener">Rate limits page</a>) is more predictable, though you pay per token.</p>
<p><strong>Related guides on this site:</strong></p>
<ul><li>/why-claude-code-stops-mid-automation-and-how-to-fix-it/</li><li>/claude-code-cron-job-setup-run-automations-overnight/</li><li>/claude-code-checkpoints-how-rollback-works-in-automations/</li></ul>
<h2 id="faq">FAQ</h2>
<h3 id="what-does-claude-code-rate-limit-reached-error-mean">What does "claude code rate limit reached error" mean?</h3>
<p>You hit one of three limits: per-minute API throttling, your plan's 5-hour or weekly allowance, or a spend cap. The message text tells you which. Only throttles and 529 overloads clear up on retry (<a href="https://code.claude.com/docs/en/errors" rel="noopener">errors</a>).</p>
<h3 id="is-rate-limit-exceeded-the-same-as-running-out-of-my-plan">Is "rate limit exceeded" the same as running out of my plan?</h3>
<p>Not always. A <code>Request rejected (429)</code> on an API key is usually a per-minute token bucket, which refills continuously. "You've hit your session limit" is a plan allowance with a fixed reset time.</p>
<h3 id="how-long-until-a-claude-code-rate-limit-resets">How long until a Claude Code rate limit resets?</h3>
<p>API throttles refill continuously. The <code>retry-after</code> header gives the exact number of seconds. Subscription limits show the reset time in the message, and the tier spend cap resets at 00:00 UTC on the first of the next month (<a href="https://platform.claude.com/docs/en/api/rate-limits" rel="noopener">rate limits</a>).</p>
<h3 id="does-a-529-overloaded-error-use-up-my-quota">Does a 529 overloaded error use up my quota?</h3>
<p>No. The Claude Code errors reference says a 529 is not your usage limit and doesn't count against your quota. Retry later, or switch models with <code>/model</code>, since capacity is tracked per model.</p>
<h2 id="sources">Sources</h2>
<ul><li><a href="https://platform.claude.com/docs/en/api/rate-limits" rel="noopener">Rate limits – Claude API docs</a></li><li><a href="https://platform.claude.com/docs/en/api/errors" rel="noopener">Claude API errors</a></li><li><a href="https://code.claude.com/docs/en/errors" rel="noopener">Claude Code errors reference</a></li><li><a href="https://code.claude.com/docs/en/costs" rel="noopener">Claude Code: Manage costs effectively</a></li><li><a href="https://code.claude.com/docs/en/headless" rel="noopener">Claude Code: Run programmatically (headless)</a></li><li><a href="https://code.claude.com/docs/en/vs-code" rel="noopener">Claude Code: Use Claude Code in VS Code</a></li><li><a href="https://claude.com/pricing" rel="noopener">Claude plans and pricing</a></li><li><a href="https://support.claude.com/en/articles/12429409-extra-usage-for-paid-claude-plans" rel="noopener">Usage credits for paid Claude plans</a></li><li><a href="https://status.claude.com" rel="noopener">Claude status</a></li><li><a href="https://arxiv.org/abs/2311.09735" rel="noopener">GEO: Generative Engine Optimization (arXiv 2311.09735)</a></li></ul><p>Originally published at <a href="https://en.aobaworks.com/claude-code-rate-limit-reached-retry-setup-for-cron-jobs/">Unattended</a>.</p>]]></content:encoded><enclosure url="https://en.aobaworks.com/og/claude-code-rate-limit-reached-retry-setup-for-cron-jobs.png" type="image/png" length="46651"/><media:content url="https://en.aobaworks.com/og/claude-code-rate-limit-reached-retry-setup-for-cron-jobs.png" medium="image" type="image/png" width="1200" height="630"/></item><item><title>Claude Code Hooks Examples: 7 Copy-Paste Scripts</title><link>https://en.aobaworks.com/claude-code-hooks-examples-7-copy-paste-scripts/</link><guid isPermaLink="true">https://en.aobaworks.com/claude-code-hooks-examples-7-copy-paste-scripts/</guid><pubDate>Sat, 17 Oct 2026 03:00:00 GMT</pubDate><dc:creator>Aoba Works</dc:creator><description>Claude Code hooks examples you can copy: 7 working scripts for guardrails, Stop-hook stall prevention, and usage logging, with settings.json and fixes.</description><category>claude code</category><category>hooks</category><category>automation</category><category>guardrails</category><category>logging</category><category>ai agents</category><content:encoded><![CDATA[<figure><img src="https://en.aobaworks.com/og/claude-code-hooks-examples-7-copy-paste-scripts.png" alt="Claude Code Hooks Examples: 7 Copy-Paste Scripts" width="1200" height="630"></figure><p class="meta ai-note">How this guide was made: drafted with AI assistance from the official sources linked below, then passed an automated editorial check before publishing. <a href="https://en.aobaworks.com/about/#how-guides-are-made">Our process</a></p><p>Claude Code hooks are shell commands that run at fixed points in a session. Common ones run before a tool call (<code>PreToolUse</code>), after one (<code>PostToolUse</code>), when Claude tries to finish (<code>Stop</code>) and when a session ends (<code>SessionEnd</code>). This guide has seven copy-paste scripts: two guardrails that block dangerous commands and protected files, three stall-prevention hooks (test gate, "waiting for you" alerts, context at startup) and two logging hooks that record every tool call and session. Each script is a small bash file in <code>.claude/hooks/</code>, wired up in <code>.claude/settings.json</code> (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>).</p>
<h2 id="which-option-is-right-for-you">Which option is right for you?</h2>
<p>Start with the one hook that fixes the problem you have today, then add others as you need them.</p>
<div class="table-wrap"><table><thead><tr><th>Your situation</th><th>What to pick first</th><th>First page to open</th></tr></thead><tbody><tr><td>Beginner, first hook ever</td><td>Script 4 (notification when Claude is waiting). It changes nothing and is easy to check</td><td><a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener">Hooks guide</a></td></tr><tr><td>Daily work, Claude runs commands unattended</td><td>Scripts 1 + 2 (guardrails)</td><td><a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference: PreToolUse</a></td></tr><tr><td>Overnight or cron runs that stop early</td><td>Script 3 (Stop test gate) + Script 5 (tool log)</td><td>/claude-code-cron-job-setup-run-automations-overnight/</td></tr><tr><td>Watching your budget or usage</td><td>Scripts 5 + 6 (logging)</td><td>/claude-code-how-to-see-token-usage-and-reduce-it/</td></tr></tbody></table></div>
<p>Why these picks:</p>
<ul><li><strong>Beginner:</strong> a notification hook can't block anything, so if it's wrong, nothing breaks.</li><li><strong>Daily use:</strong> guardrails are deterministic. They always run, so you don't depend on the model remembering a rule (<a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener">Hooks guide</a>).</li><li><strong>Budget:</strong> logs show you what the agent actually did before you change plans or prompts.</li></ul>
<p>If you haven't read about the events yet, start with /claude-code-hooks-lifecycle-every-event-explained/.</p>
<h2 id="how-hooks-are-wired-the-60-second-version">How hooks are wired: the 60-second version</h2>
<p>Every hook is a JSON entry under an event name in a settings file. Claude Code sends event data to the script on stdin, and the script's exit code or JSON output controls what happens next (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>).</p>
<p>Key facts from the official reference:</p>
<ul><li><strong>Where:</strong> <code>~/.claude/settings.json</code> (all your projects), <code>.claude/settings.json</code> (project, can be committed) or <code>.claude/settings.local.json</code> (project, gitignored).</li><li><strong>Input on stdin:</strong> <code>session_id</code>, <code>transcript_path</code>, <code>cwd</code>, <code>permission_mode</code>, <code>hook_event_name</code>, plus event fields like <code>tool_name</code> and <code>tool_input</code>.</li><li><strong>Exit codes:</strong> <code>0</code> = success, and any JSON output is applied. <code>2</code> = blocking error on events that support blocking. Any other code = non-blocking error, and the action still goes ahead.</li><li><strong>Timeout:</strong> 600 seconds by default for command hooks. You can change it with <code>"timeout"</code>.</li><li><strong>Matcher:</strong> <code>"Bash"</code>, <code>"Edit|Write"</code>, or a regex. Leave it empty or use <code>"*"</code> to match everything.</li><li><strong>Paths:</strong> use <code>${CLAUDE_PROJECT_DIR}</code> so scripts work from any working directory.</li></ul>
<p>All scripts below need <a href="https://jqlang.org/" rel="noopener"><code>jq</code></a> to parse the JSON on stdin.</p>
<h3 id="setup-steps-do-this-once">Setup steps (do this once)</h3>
<ol><li>In your project root, run <code>mkdir -p .claude/hooks .claude/logs</code>.</li><li>Save each script below into <code>.claude/hooks/</code> and make it executable: <code>chmod +x .claude/hooks/*.sh</code>.</li><li>Add the matching JSON block to <code>.claude/settings.json</code> under the top-level <code>"hooks"</code> key. If you use several hooks, merge them into one <code>"hooks"</code> object.</li><li>In Claude Code, type <strong><code>/hooks</code></strong>. The menu is a read-only browser that shows each hook, its event and which file it came from (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>).</li><li>Trigger the event on purpose (for example, ask Claude to run <code>rm -rf /tmp/x</code>) and check that the hook fires.</li></ol>
<h2 id="guardrail-scripts-1-2-stop-damage-before-it-happens">Guardrail scripts (1–2): stop damage before it happens</h2>
<p>Guardrails use <code>PreToolUse</code>, which runs before a tool call and can allow, deny, ask or defer it (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>).</p>
<h3 id="script-1-block-destructive-shell-commands">Script 1: Block destructive shell commands</h3>
<p><code>.claude/hooks/block-dangerous.sh</code>:</p>
<pre data-lang="bash"><code class="language-bash">#!/usr/bin/env bash
# Example guardrail: deny risky Bash commands before they run.
INPUT=$(cat)
CMD=$(echo &quot;$INPUT&quot; | jq -r &#x27;.tool_input.command // empty&#x27;)

if echo &quot;$CMD&quot; | grep -Eq &#x27;rm -rf|git push --force|git reset --hard|DROP TABLE&#x27;; then
  jq -n --arg c &quot;$CMD&quot; &#x27;{
    hookSpecificOutput: {
      hookEventName: &quot;PreToolUse&quot;,
      permissionDecision: &quot;deny&quot;,
      permissionDecisionReason: (&quot;Blocked by guardrail hook: &quot; + $c)
    }
  }&#x27;
fi
exit 0</code></pre>
<pre data-lang="json"><code class="language-json">{
  &quot;hooks&quot;: {
    &quot;PreToolUse&quot;: [
      { &quot;matcher&quot;: &quot;Bash&quot;,
        &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;${CLAUDE_PROJECT_DIR}/.claude/hooks/block-dangerous.sh&quot; }] }
    ]
  }
}</code></pre>
<p>The <code>permissionDecision: "deny"</code> output is the documented way to block a call. If the script prints nothing and exits 0, the normal permission flow applies (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>). <strong>Limitation:</strong> pattern matching is easy to get around (for example, <code>rm -r -f</code>). Treat it as a seatbelt, not a sandbox, and keep your normal <a href="https://code.claude.com/docs/en/permissions" rel="noopener">permission rules</a> too.</p>
<h3 id="script-2-protect-secrets-and-lock-files">Script 2: Protect secrets and lock files</h3>
<p><code>.claude/hooks/protect-files.sh</code>:</p>
<pre data-lang="bash"><code class="language-bash">#!/usr/bin/env bash
# Example: refuse edits to sensitive paths. Exit 2 = block; stderr is the reason.
FILE=$(jq -r &#x27;.tool_input.file_path // empty&#x27;)
for p in &quot;.env&quot; &quot;package-lock.json&quot; &quot;.git/&quot; &quot;secrets/&quot;; do
  if [[ &quot;$FILE&quot; == *&quot;$p&quot;* ]]; then
    echo &quot;Edits to $FILE are blocked (protected path: $p).&quot; &gt;&amp;2
    exit 2
  fi
done
exit 0</code></pre>
<p>Matcher: <code>"Edit|Write"</code>. On <code>PreToolUse</code>, exit code 2 blocks the call, and your stderr message goes to Claude as the reason, so it can change course instead of retrying blindly (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>).</p>
<h2 id="stall-prevention-scripts-3-5-keep-unattended-runs-moving">Stall-prevention scripts (3–5): keep unattended runs moving</h2>
<p>These hooks handle the three usual stalls: Claude declaring "done" too early, Claude sitting at a prompt while you're away, and Claude starting without context. For the wider set of causes, see /why-claude-code-stops-mid-automation-and-how-to-fix-it/.</p>
<h3 id="script-3-don-t-let-claude-stop-while-tests-fail">Script 3: Don't let Claude stop while tests fail</h3>
<p><code>.claude/hooks/stop-gate.sh</code>:</p>
<pre data-lang="bash"><code class="language-bash">#!/usr/bin/env bash
# Example Stop gate: block &quot;done&quot; once if tests fail.
INPUT=$(cat)
# Loop guard: if we already forced a continuation, let Claude stop.
if [ &quot;$(echo &quot;$INPUT&quot; | jq -r &#x27;.stop_hook_active // false&#x27;)&quot; = &quot;true&quot; ]; then
  exit 0
fi
cd &quot;$CLAUDE_PROJECT_DIR&quot; || exit 0
if ! npm test --silent &gt; .claude/logs/last-test.log 2&gt;&amp;1; then
  jq -n &#x27;{decision: &quot;block&quot;,
          reason: &quot;Tests are failing. Read .claude/logs/last-test.log, fix the failures, then finish.&quot;}&#x27;
fi
exit 0</code></pre>
<pre data-lang="json"><code class="language-json">&quot;Stop&quot;: [
  { &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;${CLAUDE_PROJECT_DIR}/.claude/hooks/stop-gate.sh&quot;, &quot;timeout&quot;: 300 }] }
]</code></pre>
<p>On <code>Stop</code>, <code>decision: "block"</code> with a <code>reason</code> keeps Claude working, and the reason becomes its next instruction (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>). The <code>stop_hook_active</code> field is <code>true</code> when Claude is already continuing because of a Stop hook. Check it, or you can create a loop that never ends (<a href="https://www.cubic.dev/blog/claude-code-hooks" rel="noopener">cubic</a>, <a href="https://claudefa.st/blog/tools/hooks/stop-hook-task-enforcement" rel="noopener">claudefa.st</a>). One third-party write-up says Claude Code ends the turn itself after eight blocks in a row (<a href="https://www.cubic.dev/blog/claude-code-hooks" rel="noopener">cubic</a>). Don't rely on that. <strong>Not for:</strong> projects whose test suite takes longer than your timeout. Swap in a faster check such as lint or type-check.</p>
<h3 id="script-4-get-pinged-when-claude-is-waiting-for-you">Script 4: Get pinged when Claude is waiting for you</h3>
<p><code>.claude/hooks/notify.sh</code>:</p>
<pre data-lang="bash"><code class="language-bash">#!/usr/bin/env bash
# Example: log + desktop alert when Claude needs input.
INPUT=$(cat)
MSG=$(echo &quot;$INPUT&quot; | jq -r &#x27;.message // &quot;Claude Code needs your input&quot;&#x27;)
echo &quot;$(date -u +%FT%TZ) $MSG&quot; &gt;&gt; &quot;$CLAUDE_PROJECT_DIR/.claude/logs/waiting.log&quot;
if command -v osascript &gt;/dev/null; then
  osascript -e &quot;display notification \&quot;$MSG\&quot; with title \&quot;Claude Code\&quot;&quot;
elif command -v notify-send &gt;/dev/null; then
  notify-send &quot;Claude Code&quot; &quot;$MSG&quot;
fi
exit 0</code></pre>
<p>Put it under the <code>Notification</code> event with matcher <code>"permission_prompt|idle_prompt"</code>. Both are documented notification types (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>). On a headless server the desktop alert does nothing, but <code>waiting.log</code> still shows you where an overnight run got stuck.</p>
<h3 id="script-5-bonus-context-brief-claude-at-session-start">Script 5 (bonus context): Brief Claude at session start</h3>
<p><code>.claude/hooks/session-context.sh</code>:</p>
<pre data-lang="bash"><code class="language-bash">#!/usr/bin/env bash
# Example: give Claude the current repo state at startup.
cd &quot;$CLAUDE_PROJECT_DIR&quot; || exit 0
echo &quot;Branch: $(git branch --show-current)&quot;
echo &quot;Uncommitted files:&quot;; git status --short | head -20
[ -f TODO.md ] &amp;&amp; { echo &quot;Open TODOs:&quot;; head -20 TODO.md; }
exit 0</code></pre>
<p>On <code>SessionStart</code>, plain stdout is added to Claude's context. That's different from most events, where it goes to the debug log (<a href="https://www.cubic.dev/blog/claude-code-hooks" rel="noopener">cubic</a>). Keep it short: stdout fields are capped at 10,000 characters, and anything longer is saved to a file with only a preview shown (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>).</p>
<h2 id="usage-logging-scripts-6-7-know-what-the-agent-actually-did">Usage-logging scripts (6–7): know what the agent actually did</h2>
<p>Logging hooks add one JSON line per event to a file, so you can review a run afterwards without reading the whole transcript.</p>
<h3 id="script-6-log-every-tool-call">Script 6: Log every tool call</h3>
<p><code>.claude/hooks/log-tools.sh</code>:</p>
<pre data-lang="bash"><code class="language-bash">#!/usr/bin/env bash
# Example: one JSON line per tool call.
jq -c &#x27;{ts: (now|todate), session: .session_id, tool: .tool_name,
        target: (.tool_input.command // .tool_input.file_path // .tool_input.url // null)}&#x27; \
  &gt;&gt; &quot;$CLAUDE_PROJECT_DIR/.claude/logs/tools.jsonl&quot;
exit 0</code></pre>
<pre data-lang="json"><code class="language-json">&quot;PostToolUse&quot;: [
  { &quot;matcher&quot;: &quot;*&quot;,
    &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;${CLAUDE_PROJECT_DIR}/.claude/hooks/log-tools.sh&quot;, &quot;async&quot;: true }] }
]</code></pre>
<p><code>"async": true</code> is a documented handler option (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>). Use it so logging doesn't slow each tool call down.</p>
<h3 id="script-7-session-summary-on-exit">Script 7: Session summary on exit</h3>
<p><code>.claude/hooks/session-summary.sh</code>:</p>
<pre data-lang="bash"><code class="language-bash">#!/usr/bin/env bash
# Example: summarize a session when it ends.
INPUT=$(cat)
SID=$(echo &quot;$INPUT&quot; | jq -r &#x27;.session_id&#x27;)
REASON=$(echo &quot;$INPUT&quot; | jq -r &#x27;.reason // &quot;unknown&quot;&#x27;)
LOG=&quot;$CLAUDE_PROJECT_DIR/.claude/logs/tools.jsonl&quot;
COUNT=$(grep -c &quot;\&quot;$SID\&quot;&quot; &quot;$LOG&quot; 2&gt;/dev/null || echo 0)
TOP=$(grep &quot;\&quot;$SID\&quot;&quot; &quot;$LOG&quot; 2&gt;/dev/null | jq -r &#x27;.tool&#x27; | sort | uniq -c | sort -rn | head -3 | tr &#x27;\n&#x27; &#x27; &#x27;)
echo &quot;$(date -u +%FT%TZ) session=$SID reason=$REASON tool_calls=$COUNT top=[$TOP] transcript=$(echo &quot;$INPUT&quot; | jq -r &#x27;.transcript_path&#x27;)&quot; \
  &gt;&gt; &quot;$CLAUDE_PROJECT_DIR/.claude/logs/sessions.log&quot;
exit 0</code></pre>
<p>Register it under <code>SessionEnd</code>. Documented end reasons include <code>clear</code>, <code>logout</code>, <code>prompt_input_exit</code> and <code>other</code> (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>). <strong>Limitation:</strong> this counts tool calls, not tokens or cost. For token numbers, use the tools in /claude-code-how-to-see-token-usage-and-reduce-it/.</p>
<h2 id="common-mistakes-and-fixes">Common mistakes and fixes</h2>
<p>Most broken hooks come down to one of five problems:</p>
<div class="table-wrap"><table><thead><tr><th>Symptom</th><th>Likely cause</th><th>Fix</th></tr></thead><tbody><tr><td>Hook never fires</td><td>Wrong event name or matcher (names are case-sensitive, e.g. <code>Bash</code>, `Edit\</td><td>Write`)</td><td>Open <code>/hooks</code> and confirm the event, matcher and source file</td></tr><tr><td>"hook error" notice</td><td>Script isn't executable or <code>jq</code> is missing</td><td>Run <code>chmod +x</code>, install <code>jq</code>, and test with `echo '{}' \</td><td>./script.sh`</td></tr><tr><td>JSON output ignored</td><td>Exited with a code other than 0, or extra text was printed before the JSON</td><td>Print only the JSON, then <code>exit 0</code></td></tr><tr><td>Claude never finishes</td><td>Stop hook ignores <code>stop_hook_active</code></td><td>Add the loop guard from Script 3</td></tr><tr><td>Works in one folder, not another</td><td>Relative paths</td><td>Use <code>${CLAUDE_PROJECT_DIR}</code></td></tr></tbody></table></div>
<p>For anything else, turn on debug logging. It shows hook runs, full stderr and JSON parse failures (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>, <a href="https://code.claude.com/docs/en/debug-logs" rel="noopener">debug logs</a>). If a hook goes wrong and Claude edits files it shouldn't, /claude-code-checkpoints-how-rollback-works-in-automations/ explains how to roll back.</p>
<h2 id="next-steps">Next steps</h2>
<p>The fastest win is one guardrail and one log today, then a test gate once both are working.</p>
<p><strong>Today, on your own:</strong></p>
<ol><li>Add Script 1 and Script 6 to one project, then use <code>/hooks</code> to check both are registered.</li><li>Run a normal task and read <code>.claude/logs/tools.jsonl</code> afterwards.</li><li>When it looks right, add Script 3 with your fastest test command.</li></ol>
<p><strong>If you'd rather not build this yourself:</strong></p>
<ul><li><strong>Claude Academy's "Claude Code in Action"</strong>: a free course of about one hour and 9 lessons, including using hooks to enforce rules (<a href="https://anthropic.skilljar.com/claude-code-in-action" rel="noopener">Skilljar</a>, <a href="https://www.classcentral.com/course/anthropic-academy-claude-code-in-action-536160" rel="noopener">Class Central</a>). Good if you want structured basics. It isn't for you if you need help with your specific setup, because it's general training.</li><li><strong>Claude Team plan</strong>: $25/seat/month, or $20 billed annually (as of October 2026; check the <a href="https://claude.com/pricing" rel="noopener">official page</a>). Useful if a small team wants shared project settings, and admins can restrict hooks with managed settings such as <code>allowManagedHooksOnly</code> (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>). Not worth it for a solo builder: Pro ($20/month, or $17 billed annually, as of October 2026; see <a href="https://claude.com/pricing" rel="noopener">Claude pricing</a>) already includes Claude Code. <strong>Limitation:</strong> no plan writes your hooks for you.</li></ul>
<p><strong>Related guides on this site:</strong></p>
<ul><li>/claude-code-hooks-lifecycle-every-event-explained/</li><li>/why-claude-code-stops-mid-automation-and-how-to-fix-it/</li><li>/claude-code-cron-job-setup-run-automations-overnight/</li></ul>
<h2 id="faq">FAQ</h2>
<h3 id="where-do-i-put-claude-code-hooks">Where do I put Claude Code hooks?</h3>
<p>Put them in <code>.claude/settings.json</code> for a project you share, <code>.claude/settings.local.json</code> for personal project hooks, or <code>~/.claude/settings.json</code> for all your projects. The scripts themselves can live anywhere, but <code>.claude/hooks/</code> plus <code>${CLAUDE_PROJECT_DIR}</code> keeps paths reliable (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>).</p>
<h3 id="how-do-i-block-a-command-with-a-hook">How do I block a command with a hook?</h3>
<p>Use a <code>PreToolUse</code> hook that either prints JSON with <code>permissionDecision: "deny"</code> or exits with code 2 and a reason on stderr. Both stop the tool call, and Claude sees the reason (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>).</p>
<h3 id="why-does-my-stop-hook-make-claude-loop-forever">Why does my Stop hook make Claude loop forever?</h3>
<p>The hook blocks every stop, even after Claude has already been forced to continue. Read <code>stop_hook_active</code> from stdin and exit 0 when it's <code>true</code> (<a href="https://www.cubic.dev/blog/claude-code-hooks" rel="noopener">cubic</a>).</p>
<h3 id="do-hooks-slow-claude-code-down">Do hooks slow Claude Code down?</h3>
<p>Synchronous hooks run inline, and command hooks can run for up to 600 seconds by default. Keep checks fast, set a lower <code>timeout</code>, and mark logging hooks <code>"async": true</code> (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>).</p>
<h2 id="sources">Sources</h2>
<ul><li><a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener">Automate actions with hooks (Hooks guide) – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/permissions" rel="noopener">Permissions – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/debug-logs" rel="noopener">Debug logs – Claude Code Docs</a></li><li><a href="https://www.cubic.dev/blog/claude-code-hooks" rel="noopener">Claude Code hooks: every event, with examples – cubic</a></li><li><a href="https://claudefa.st/blog/tools/hooks/stop-hook-task-enforcement" rel="noopener">Claude Code Stop Hook: Force Task Completion – claudefa.st</a></li><li><a href="https://claude.com/pricing" rel="noopener">Claude pricing</a></li><li><a href="https://anthropic.skilljar.com/claude-code-in-action" rel="noopener">Claude Code in Action – Anthropic Courses</a></li><li><a href="https://www.classcentral.com/course/anthropic-academy-claude-code-in-action-536160" rel="noopener">Claude Code in Action – Class Central</a></li><li><a href="https://jqlang.org/" rel="noopener">jq</a></li></ul><p>Originally published at <a href="https://en.aobaworks.com/claude-code-hooks-examples-7-copy-paste-scripts/">Unattended</a>.</p>]]></content:encoded><enclosure url="https://en.aobaworks.com/og/claude-code-hooks-examples-7-copy-paste-scripts.png" type="image/png" length="552415"/><media:content url="https://en.aobaworks.com/og/claude-code-hooks-examples-7-copy-paste-scripts.png" medium="image" type="image/png" width="1200" height="630"/></item><item><title>Claude Code: How to See Token Usage (and Reduce It)</title><link>https://en.aobaworks.com/claude-code-how-to-see-token-usage-and-reduce-it/</link><guid isPermaLink="true">https://en.aobaworks.com/claude-code-how-to-see-token-usage-and-reduce-it/</guid><pubDate>Sat, 17 Oct 2026 03:00:00 GMT</pubDate><dc:creator>Aoba Works</dc:creator><description>Claude Code how to see token usage: run /usage in a session, read total_cost_usd from headless JSON runs, check the Console Usage page, and set cost alerts.</description><category>claude code</category><category>token usage</category><category>cost tracking</category><category>headless</category><category>hooks</category><category>claude console</category><content:encoded><![CDATA[<figure><img src="https://en.aobaworks.com/og/claude-code-how-to-see-token-usage-and-reduce-it.png" alt="Claude Code: How to See Token Usage (and Reduce It)" width="1200" height="630"></figure><p class="meta ai-note">How this guide was made: drafted with AI assistance from the official sources linked below, then passed an automated editorial check before publishing. <a href="https://en.aobaworks.com/about/#how-guides-are-made">Our process</a></p><p>To see token usage in Claude Code, type <code>/usage</code> in an interactive session. The Session block lists your input, output and cache tokens per model, plus an estimated dollar total (<a href="https://code.claude.com/docs/en/costs" rel="noopener">Claude Code docs: Manage costs</a>). For headless runs, add <code>--output-format json</code> to <code>claude -p</code>. The result then includes <code>total_cost_usd</code> and a cost breakdown per model, so a script can log every run (<a href="https://code.claude.com/docs/en/headless" rel="noopener">Run Claude Code programmatically</a>). For the real bill on an API key, check the Usage page in the <a href="https://platform.claude.com/usage" rel="noopener">Claude Console</a>. Claude Code's own figures are client-side estimates.</p>
<h2 id="which-option-is-right-for-you">Which option is right for you?</h2>
<p>The right place to check depends on how you pay and whether anyone watches the run.</p>
<div class="table-wrap"><table><thead><tr><th>Your situation</th><th>What to pick first</th><th>First page to open</th></tr></thead><tbody><tr><td>Beginner, interactive sessions only</td><td><code>/usage</code> inside Claude Code</td><td><a href="https://code.claude.com/docs/en/costs" rel="noopener">Manage costs</a></td></tr><tr><td>Daily work on a Pro/Max subscription</td><td><code>/usage</code> plan bars plus the 24h/7d breakdown</td><td><a href="https://code.claude.com/docs/en/costs" rel="noopener">Manage costs → Plan usage breakdown</a></td></tr><tr><td>Recurring headless jobs (cron, CI)</td><td><code>--output-format json</code> + <code>--max-budget-usd</code></td><td><a href="https://code.claude.com/docs/en/headless" rel="noopener">Run Claude Code programmatically</a></td></tr><tr><td>Tight budget on an API key</td><td>Workspace <strong>Spend limits</strong> in the Console</td><td><a href="https://platform.claude.com/docs/en/build-with-claude/workspaces" rel="noopener">Workspaces</a></td></tr><tr><td>Want numbers for a dashboard</td><td>Usage and Cost Admin API or OpenTelemetry</td><td><a href="https://platform.claude.com/docs/en/build-with-claude/usage-cost-api" rel="noopener">Usage and Cost API</a></td></tr></tbody></table></div>
<ul><li><strong>Beginner:</strong> <code>/usage</code> needs no setup. It is the quickest way to learn what a normal session costs you.</li><li><strong>Daily work:</strong> on a subscription, the dollar figure doesn't affect your bill. The plan bars and the attribution breakdown are what show how fast you're using your limits.</li><li><strong>Budget:</strong> a spend limit in the Console is enforced by Anthropic. Local estimates can't cap anything.</li></ul>
<h2 id="how-to-read-usage-and-where-cost-went">How to read /usage (and where /cost went)</h2>
<p><code>/usage</code> is the in-session command for token and cost numbers. Current docs describe everything under it.</p>
<p>The changelog still names <code>/cost</code> in recent entries, such as the v2.1.296 note about cache-read pricing (<a href="https://code.claude.com/docs/en/changelog" rel="noopener">changelog</a>). If <code>/cost</code> doesn't do what an older tutorial says, use <code>/usage</code>.</p>
<h3 id="steps">Steps</h3>
<ol><li>Start a session with <code>claude</code> and do some work.</li><li>Type <code>/usage</code> and press Enter. It runs right away, even while Claude is responding (<a href="https://code.claude.com/docs/en/commands" rel="noopener">Commands</a>).</li><li>Read the <strong>Session</strong> block: <code>Total cost</code>, <code>Total duration (API)</code>, <code>Total duration (wall)</code>, <code>Total code changes</code>, and <strong>Usage by model</strong>. Usage by model shows input, output, cache-read and cache-write tokens for each model (<a href="https://code.claude.com/docs/en/costs" rel="noopener">Manage costs</a>).</li><li>On v2.1.251 or later, check the <strong>Prompt cache (main)</strong> line. It shows the share of input tokens that came from cache and the number of cache misses. A high miss count usually means you paid full price to re-read context.</li><li>On a Pro, Max, Team or Enterprise plan, scroll to the breakdown. It shows usage split across skills, subagents, plugins and MCP servers, and flags any behavior that accounts for 10% or more of recent usage. Press <code>d</code> or <code>w</code> to switch between 24 hours and 7 days.</li></ol>
<h3 id="what-the-numbers-mean">What the numbers mean</h3>
<ul><li>The dollar figure is calculated <strong>locally at list price</strong>. It is an estimate, not an invoice (<a href="https://code.claude.com/docs/en/costs" rel="noopener">Manage costs</a>).</li><li>On a subscription, the Session cost line doesn't matter for billing. The plan bars do.</li><li><code>/usage</code> reads local session history only. Usage from other machines or claude.ai is not included.</li><li>The token count next to the spinner (for example <code>↓ 5.5k tokens</code>) counts only the output of the current turn. It will never match the session totals.</li></ul>
<h3 id="common-mistakes-and-fixes">Common mistakes and fixes</h3>
<ul><li><strong>"My total went back to $0."</strong> <code>/clear</code> starts a new session and resets the Session block (v2.1.211 and later). Note the number before you clear.</li><li><strong>"<code>/usage</code> shows old bars."</strong> If the usage endpoint is rate-limited, you'll see <code>Showing last-known usage</code>. Press <code>r</code> to retry.</li><li><strong>"The cost doesn't match my bill."</strong> That's expected. Use the Console Usage page for the real figure.</li></ul>
<h2 id="how-to-track-token-burn-in-headless-runs">How to track token burn in headless runs</h2>
<p>For unattended work, ask for JSON output on every <code>claude -p</code> call and keep a log of <code>total_cost_usd</code>.</p>
<h3 id="steps-2">Steps</h3>
<ol><li>Add <code>--output-format json</code>:</li></ol>
<p>``<code>bash claude -p "Summarize open TODOs in src/" --output-format json | jq '.total_cost_usd' </code>`<code> The payload contains </code>result<code>, </code>session_id<code>, </code>total_cost_usd` and a per-model cost breakdown (<a href="https://code.claude.com/docs/en/headless" rel="noopener">headless docs</a>).</p>
<ol><li>Add a hard stop with <code>--max-budget-usd 5.00</code>. The run stops once <em>estimated</em> spend reaches the cap, and spend from subagents counts toward it. Spend can go past the cap, so leave some headroom (<a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference</a>).</li><li>Add <code>--max-turns</code> to stop runaway loops. When the limit is hit, the run exits with an error.</li><li>In CI, add <code>--bare</code>. It skips loading hooks, plugins, MCP servers and CLAUDE.md, which keeps startup context small and predictable. Bare mode does not use your subscription login, so set <code>ANTHROPIC_API_KEY</code> (<a href="https://code.claude.com/docs/en/headless" rel="noopener">headless docs</a>).</li></ol>
<h3 id="example-a-cost-logging-wrapper">Example: a cost-logging wrapper</h3>
<p>This is an illustrative script, not an official tool. Change the paths and limits to suit your setup.</p>
<pre data-lang="bash"><code class="language-bash">#!/usr/bin/env bash
# run-claude.sh &quot;&lt;prompt&gt;&quot;
ALERT_AT=2.00
out=$(claude -p &quot;$1&quot; --output-format json --max-budget-usd 3.00 --max-turns 30) || status=$?
cost=$(echo &quot;$out&quot; | jq -r &#x27;.total_cost_usd // 0&#x27;)
sid=$(echo &quot;$out&quot; | jq -r &#x27;.session_id // &quot;none&quot;&#x27;)
echo &quot;$(date -u +%FT%TZ),$sid,$cost,${status:-0}&quot; &gt;&gt; &quot;$HOME/claude-costs.csv&quot;
if (( $(echo &quot;$cost &gt; $ALERT_AT&quot; | bc -l) )); then
  echo &quot;Claude run $sid cost \$$cost (alert at \$$ALERT_AT)&quot; &gt;&amp;2
  # send to a channel you own, e.g. curl -X POST &quot;$YOUR_WEBHOOK_URL&quot; -d &quot;...&quot;
fi</code></pre>
<p>The script still logs when the run fails, because <code>|| status=$?</code> stops a non-zero exit from killing it. Failed runs still use tokens, and the SDK docs note that error results carry cost too (<a href="https://code.claude.com/docs/en/agent-sdk/cost-tracking" rel="noopener">Track cost and usage</a>). To schedule the script, see <a href="https://en.aobaworks.com/claude-code-cron-job-setup-run-automations-overnight/">Claude Code Cron Job Setup: Run Automations Overnight</a>. If runs stop early because of <code>--max-turns</code>, read <a href="https://en.aobaworks.com/why-claude-code-stops-mid-automation-and-how-to-fix-it/">Why Claude Code Stops Mid-Automation (and How to Fix It)</a>.</p>
<h3 id="common-mistakes-and-fixes-2">Common mistakes and fixes</h3>
<ul><li><strong>Summing totals across <code>--resume</code> runs.</strong> A resumed run reports the whole conversation's total, including earlier runs (<a href="https://code.claude.com/docs/en/headless" rel="noopener">headless docs</a>). Take the latest value. Don't add them up.</li><li><strong>Trusting <code>usage</code> for subagent-heavy jobs.</strong> The <code>usage</code> field leaves out subagent tokens. <code>total_cost_usd</code> and <code>modelUsage</code> include them (<a href="https://code.claude.com/docs/en/agent-sdk/cost-tracking" rel="noopener">Track cost and usage</a>).</li><li><strong>Billing customers from these fields.</strong> Anthropic says not to. They are client-side estimates.</li></ul>
<h2 id="how-to-wire-usage-checks-into-hooks">How to wire usage checks into hooks</h2>
<p>Hooks are good for cutting tokens, but they can't produce a per-run cost alert on their own. According to the <a href="https://code.claude.com/docs/en/hooks" rel="noopener">hooks reference</a>, <code>Stop</code> and <code>SessionEnd</code> hooks receive <code>session_id</code>, <code>transcript_path</code>, <code>cwd</code> and a <code>reason</code>, but no token or cost data.</p>
<p>What does work:</p>
<ol><li><strong>Alerting:</strong> put the cost check in the wrapper script above, which runs after the <code>-p</code> process exits. The cost figure exists there and nowhere else.</li><li><strong>Resume warnings:</strong> per the hooks reference, a <code>SessionStart</code> hook that fires on resume receives <code>context_tokens</code>, <code>estimated_cache_write_usd</code> and <code>prompt_cache_likely_expired</code>. You can log these, or warn yourself before an expensive cold-cache resume.</li><li><strong>Cutting tokens with PreToolUse:</strong> the costs page includes an official <code>PreToolUse</code> hook on <code>Bash</code>. It rewrites <code>npm test</code>, <code>pytest</code> or <code>go test</code> so that only failure lines reach Claude. Anthropic's example says this kind of filtering can shrink context "from tens of thousands of tokens to hundreds" (<a href="https://code.claude.com/docs/en/costs" rel="noopener">Manage costs</a>). To check it's active, run <code>/hooks</code> and look under <strong>PreToolUse</strong>.</li><li><strong>Live view:</strong> a custom status line receives session cost data and can show it all the time (<a href="https://code.claude.com/docs/en/statusline" rel="noopener">Status line</a>).</li></ol>
<p>For a map of every hook event and when each one fires, see <a href="https://en.aobaworks.com/claude-code-hooks-lifecycle-every-event-explained/">Claude Code Hooks Lifecycle: Every Event, Explained</a>.</p>
<p><strong>Limitation:</strong> <code>--bare</code> skips hooks. If your CI runs use bare mode, a PreToolUse filter in <code>~/.claude</code> won't run. Pass the settings with <code>--settings</code> instead (<a href="https://code.claude.com/docs/en/headless" rel="noopener">headless docs</a>).</p>
<h2 id="where-to-see-real-billing-console-and-apis">Where to see real billing: Console and APIs</h2>
<p>For money you are actually charged on an API key, the Claude Console is the source of truth.</p>
<ul><li><strong>Usage page:</strong> go to <a href="https://platform.claude.com/usage" rel="noopener">platform.claude.com/usage</a> for usage over time. When you first sign in to Claude Code with a Console account, a <strong>Claude Code</strong> workspace is created automatically to keep that traffic separate (<a href="https://platform.claude.com/docs/en/build-with-claude/workspaces" rel="noopener">Workspaces</a>).</li><li><strong>Spend limits:</strong> open <strong>Settings → Workspaces</strong>, select the workspace, then click <strong>Spend limits</strong>. This caps monthly spend and can alert you at thresholds you choose. You can't set limits on the Default Workspace (<a href="https://platform.claude.com/docs/en/build-with-claude/workspaces" rel="noopener">Workspaces</a>).</li><li><strong>Rate limits:</strong> on the <strong>Rate limits</strong> page, choose the workspace from the <strong>Workspace</strong> dropdown and click <strong>Edit</strong> next to a model (<a href="https://platform.claude.com/docs/en/build-with-claude/workspaces" rel="noopener">Workspaces</a>).</li><li><strong>Usage and Cost Admin API:</strong> <code>/v1/organizations/usage_report/messages</code> (buckets of <code>1m</code>, <code>1h</code> or <code>1d</code>) and <code>/v1/organizations/cost_report</code> (daily). Both need an Admin API key, and <strong>the Admin API is not available to individual accounts</strong>. Data usually appears within 5 minutes (<a href="https://platform.claude.com/docs/en/build-with-claude/usage-cost-api" rel="noopener">Usage and Cost API</a>).</li><li><strong>OpenTelemetry:</strong> set <code>CLAUDE_CODE_ENABLE_TELEMETRY=1</code> to export <code>claude_code.cost.usage</code> and <code>claude_code.token.usage</code> to your own backend (<a href="https://code.claude.com/docs/en/monitoring-usage" rel="noopener">Monitoring</a>). This is worth it if you already run Grafana or similar. It's overkill for one laptop.</li></ul>
<h2 id="how-to-reduce-token-usage">How to reduce token usage</h2>
<p>Most token waste comes from context you no longer need. Anthropic lists these levers (<a href="https://code.claude.com/docs/en/costs" rel="noopener">Manage costs</a>):</p>
<ul><li><strong>Run <code>/clear</code> between unrelated tasks.</strong> It costs nothing. <code>/compact</code> has to read the whole conversation to summarize it, so it is itself a large request.</li><li><strong>Use Sonnet by default.</strong> Save Opus for hard reasoning. Set <code>model: haiku</code> for simple subagents. Leaving Opus as the default is one of the two most common causes of surprise spend Anthropic names; long sessions that never get cleared are the other.</li><li><strong>Keep CLAUDE.md under about 200 lines.</strong> Move workflow-specific instructions into skills, which load only when needed.</li><li><strong>Prefer CLI tools such as <code>gh</code> over MCP servers where possible.</strong> Turn off unused servers in <code>/mcp</code>, and run <code>/context</code> to see what's taking up space.</li><li><strong>Lower the effort level</strong> with <code>/effort</code> on simple jobs. Thinking tokens are billed as output.</li><li><strong>Be careful with agent teams.</strong> In plan mode they use about 7x the tokens of a standard session.</li><li><strong>Watch idle costs.</strong> Scheduled <code>/loop</code> tasks resend your full context on every run. Background work, such as summarizing past conversations for <code>--resume</code>, typically costs under $0.04 per session.</li></ul>
<p>For context: across enterprise deployments, Anthropic reports an average of about $13 per developer per active day, and under $30 for 90% of users (<a href="https://code.claude.com/docs/en/costs" rel="noopener">Manage costs</a>). Your numbers will differ, so measure your own first.</p>
<h2 id="next-steps">Next steps</h2>
<p><strong>DIY today:</strong></p>
<ol><li>Run <code>/usage</code> at the end of your next session and write down the total and the Prompt cache hit share.</li><li>Put <code>--output-format json --max-budget-usd</code> on one recurring <code>claude -p</code> job and log <code>total_cost_usd</code> to a CSV.</li><li>If you use an API key, set <strong>Spend limits</strong> on the Claude Code workspace in the Console.</li></ol>
<p><strong>If you'd rather not manage per-token billing yourself:</strong> a Claude subscription replaces per-token bills with plan limits. Pro is $20/month billed monthly, and Max starts at $100/month. Both include Claude Code (as of October 2026; check the <a href="https://claude.com/pricing" rel="noopener">official pricing page</a>).</p>
<ul><li><strong>Suits:</strong> people who mainly work interactively and want a predictable monthly cost.</li><li><strong>Not for:</strong> CI or <code>--bare</code> jobs that need an API key, or anyone who needs exact per-run invoices.</li><li><strong>Limitations:</strong> plan usage resets on a rolling five-hour window and a weekly window, and going past the limit requires usage credits (<a href="https://claude.com/pricing" rel="noopener">Claude pricing</a>).</li></ul>
<p><strong>Related guides on this site:</strong></p>
<ul><li><a href="https://en.aobaworks.com/claude-code-cron-job-setup-run-automations-overnight/">Claude Code Cron Job Setup: Run Automations Overnight</a></li><li><a href="https://en.aobaworks.com/claude-code-hooks-lifecycle-every-event-explained/">Claude Code Hooks Lifecycle: Every Event, Explained</a></li><li><a href="https://en.aobaworks.com/claude-code-checkpoints-how-rollback-works-in-automations/">Claude Code Checkpoints: How Rollback Works in Automations</a></li></ul>
<h2 id="faq">FAQ</h2>
<h3 id="does-claude-code-still-have-a-cost-command">Does Claude Code still have a /cost command?</h3>
<p>Current documentation puts session token and cost figures under <code>/usage</code>, though the changelog still mentions <code>/cost</code> in recent entries. If <code>/cost</code> behaves differently from an old guide, run <code>/usage</code>. You'll find the same Session block there.</p>
<h3 id="how-do-i-see-token-usage-for-claude-p-runs">How do I see token usage for <code>claude -p</code> runs?</h3>
<p>Add <code>--output-format json</code> and read <code>total_cost_usd</code> with <code>jq</code>. For live events, use <code>--output-format stream-json --verbose</code>; the last line is a <code>result</code> message that includes the cost (<a href="https://code.claude.com/docs/en/headless" rel="noopener">headless docs</a>).</p>
<h3 id="why-doesn-t-the-usage-cost-match-my-bill">Why doesn't the /usage cost match my bill?</h3>
<p>Claude Code calculates the figure locally at list price. Pricing changes and billing rules it can't model make the two drift apart. Use the Console Usage page or the Usage and Cost API for the real numbers.</p>
<h3 id="can-a-hook-stop-claude-code-when-it-gets-too-expensive">Can a hook stop Claude Code when it gets too expensive?</h3>
<p>Not directly, because <code>Stop</code> and <code>SessionEnd</code> hooks don't receive cost data. Use <code>--max-budget-usd</code> for a hard cap on headless runs, and a Console spend limit for a cap Anthropic enforces.</p>
<h3 id="do-pro-and-max-users-need-to-watch-token-costs">Do Pro and Max users need to watch token costs?</h3>
<p>Not in dollars, since usage is included in the plan. Do watch the plan bars and the 24h/7d breakdown in <code>/usage</code>, because long contexts and cache misses use up your limits faster.</p>
<h2 id="sources">Sources</h2>
<ul><li><a href="https://code.claude.com/docs/en/costs" rel="noopener">Manage costs effectively – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/headless" rel="noopener">Run Claude Code programmatically – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/commands" rel="noopener">Commands – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/statusline" rel="noopener">Customize your status line – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/monitoring-usage" rel="noopener">Monitoring usage (OpenTelemetry) – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/agent-sdk/cost-tracking" rel="noopener">Track cost and usage – Agent SDK Docs</a></li><li><a href="https://code.claude.com/docs/en/changelog" rel="noopener">Claude Code changelog</a></li><li><a href="https://platform.claude.com/docs/en/build-with-claude/usage-cost-api" rel="noopener">Usage and Cost API – Claude Platform Docs</a></li><li><a href="https://platform.claude.com/docs/en/build-with-claude/workspaces" rel="noopener">Workspaces – Claude Platform Docs</a></li><li><a href="https://platform.claude.com/usage" rel="noopener">Claude Console Usage page</a></li><li><a href="https://claude.com/pricing" rel="noopener">Claude pricing</a></li></ul><p>Originally published at <a href="https://en.aobaworks.com/claude-code-how-to-see-token-usage-and-reduce-it/">Unattended</a>.</p>]]></content:encoded><enclosure url="https://en.aobaworks.com/og/claude-code-how-to-see-token-usage-and-reduce-it.png" type="image/png" length="580489"/><media:content url="https://en.aobaworks.com/og/claude-code-how-to-see-token-usage-and-reduce-it.png" medium="image" type="image/png" width="1200" height="630"/></item><item><title>Claude Code Hooks Lifecycle: Every Event, Explained</title><link>https://en.aobaworks.com/claude-code-hooks-lifecycle-every-event-explained/</link><guid isPermaLink="true">https://en.aobaworks.com/claude-code-hooks-lifecycle-every-event-explained/</guid><pubDate>Fri, 16 Oct 2026 03:00:00 GMT</pubDate><dc:creator>Aoba Works</dc:creator><description>Claude Code hooks lifecycle explained: when each hook event fires in an automation run, and which ones stop runaway token use or silent failures.</description><category>claude code</category><category>hooks</category><category>automation</category><category>ai agents</category><category>solo builders</category><category>devops</category><content:encoded><![CDATA[<figure><img src="https://en.aobaworks.com/og/claude-code-hooks-lifecycle-every-event-explained.png" alt="Claude Code Hooks Lifecycle: Every Event, Explained" width="1200" height="630"></figure><p class="meta ai-note">How this guide was made: drafted with AI assistance from the official sources linked below, then passed an automated editorial check before publishing. <a href="https://en.aobaworks.com/about/#how-guides-are-made">Our process</a></p><p>Claude Code hooks are shell commands, HTTP calls, MCP tools or model prompts that run at fixed points in a session. A run starts with <code>SessionStart</code>, loops through <code>UserPromptSubmit</code> → <code>PreToolUse</code> → <code>PostToolUse</code>/<code>PostToolUseFailure</code> → <code>PostToolBatch</code> for each step, ends each turn with <code>Stop</code> (or <code>StopFailure</code> when the API errors), and finishes with <code>SessionEnd</code> (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>). Four of these events do most of the work in unattended automations. <code>PreToolUse</code>, <code>PostToolBatch</code> and <code>Stop</code> can halt runaway work. <code>StopFailure</code> reports runs that died quietly.</p>
<h2 id="which-option-is-right-for-you">Which option is right for you?</h2>
<p>Choose your first hook based on the problem you have now. The table below matches common situations to an event.</p>
<div class="table-wrap"><table><thead><tr><th>Your situation</th><th>What to pick first</th><th>First page to open</th></tr></thead><tbody><tr><td>Beginner, never written a hook</td><td>A <code>Notification</code> hook that alerts you when Claude is waiting</td><td><a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener">Hooks guide: Set up your first hook</a></td></tr><tr><td>Daily automation that sometimes loops or burns usage</td><td>A <code>PreToolUse</code> or <code>PostToolBatch</code> budget guard</td><td><a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference: Decision control</a></td></tr><tr><td>Overnight or cron runs that "finish" with nothing done</td><td><code>StopFailure</code> + <code>Stop</code> hooks that log and verify</td><td><a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a> and /claude-code-cron-job-setup-run-automations-overnight/</td></tr><tr><td>Tight budget (Pro plan, shared usage limits)</td><td>Command hooks only. Skip <code>prompt</code>/<code>agent</code> hooks</td><td><a href="https://claude.com/pricing" rel="noopener">Claude pricing</a></td></tr></tbody></table></div>
<p>Why each pick makes sense:</p>
<ul><li><strong>Beginner:</strong> a notification hook can't block anything, so a mistake in it won't break your session.</li><li><strong>Daily use:</strong> you can block at <code>PreToolUse</code> and <code>PostToolBatch</code> before the next model call, so you stop the spending instead of just recording it (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>).</li><li><strong>Unattended runs:</strong> <code>StopFailure</code> matches error types such as <code>rate_limit</code> and <code>billing_error</code>, which otherwise show up as a quiet exit (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>).</li><li><strong>Budget:</strong> <code>prompt</code> hooks send text to a model and <code>agent</code> hooks run a subagent, so both use model calls. Command hooks are just your own scripts and cost nothing extra.</li></ul>
<h2 id="the-lifecycle-of-one-automation-run-in-order">The lifecycle of one automation run, in order</h2>
<p>Every hook event belongs to a phase of the run. Knowing the phase tells you whether a hook can still change the outcome. The table follows the official reference, which currently lists more than 30 events (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a>).</p>
<div class="table-wrap"><table><thead><tr><th>Phase</th><th>Event</th><th>Fires when</th><th>Can block?</th></tr></thead><tbody><tr><td>Start</td><td><code>SessionStart</code></td><td>Session begins or resumes (matcher: <code>startup</code>, <code>resume</code>, <code>clear</code>, <code>compact</code>, <code>fork</code>)</td><td>No</td></tr><tr><td>Start</td><td><code>InstructionsLoaded</code></td><td>A CLAUDE.md or <code>.claude/rules/*.md</code> file is loaded</td><td>No</td></tr><tr><td>Input</td><td><code>UserPromptSubmit</code></td><td>A prompt is submitted, before Claude processes it, including turns Claude Code starts by itself</td><td>Yes</td></tr><tr><td>Tool loop</td><td><code>PreToolUse</code></td><td>Before a tool call executes</td><td>Yes</td></tr><tr><td>Tool loop</td><td><code>PermissionRequest</code> / <code>PermissionDenied</code></td><td>A call needs a permission decision / auto mode denied it</td><td>No</td></tr><tr><td>Tool loop</td><td><code>PostToolUse</code> / <code>PostToolUseFailure</code></td><td>After a tool call succeeds / fails</td><td>No (exit code), but <code>decision: "block"</code> feeds back</td></tr><tr><td>Tool loop</td><td><code>PostToolBatch</code></td><td>After a batch of parallel tool calls resolves, <strong>before the next model call</strong></td><td>Yes</td></tr><tr><td>Delegation</td><td><code>SubagentStart</code> / <code>SubagentStop</code></td><td>Subagent spawned / finished</td><td>Stop: Yes</td></tr><tr><td>Context</td><td><code>PreCompact</code> / <code>PostCompact</code></td><td>Before / after context compaction (<code>manual</code> or <code>auto</code>)</td><td>Pre: Yes</td></tr><tr><td>Turn end</td><td><code>Stop</code></td><td>Claude finishes responding</td><td>Yes</td></tr><tr><td>Turn end</td><td><code>StopFailure</code></td><td>Turn ends because of an API error</td><td>No</td></tr><tr><td>Waiting</td><td><code>Notification</code></td><td>Claude Code sends a notification (e.g. <code>permission_prompt</code>, <code>idle_prompt</code>)</td><td>No</td></tr><tr><td>End</td><td><code>SessionEnd</code></td><td>Session terminates</td><td>No</td></tr></tbody></table></div>
<p>The reference also lists events for tasks, teams, worktrees, model switches, MCP elicitation and file changes, such as <code>TaskCompleted</code>, <code>TeammateIdle</code> and <code>FileChanged</code>. Most solo setups don't need them on day one (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>).</p>
<h3 id="three-timing-facts-that-matter">Three timing facts that matter</h3>
<ul><li><strong>All matching hooks run in parallel</strong>, so you can't rely on one hook finishing before another starts (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>).</li><li><strong>Default timeouts differ by event.</strong> Most hooks get 600 seconds, <code>UserPromptSubmit</code> gets 30, and all <code>SessionEnd</code> hooks share a 1.5-second budget that can be raised to at most 60 seconds (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>). Keep cleanup work at session end small.</li><li><strong>Hooks from different places add up.</strong> User, project, local, plugin, skill and subagent hooks all run; a lower level doesn't replace a higher one (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>).</li></ul>
<h2 id="which-hooks-actually-stop-runaway-token-use">Which hooks actually stop runaway token use</h2>
<p>Only hooks that fire <em>before</em> the next model call can stop a runaway run. In practice that means <code>PreToolUse</code>, <code>PostToolBatch</code>, <code>UserPromptSubmit</code> and the universal <code>continue: false</code> field. Logging hooks like <code>PostToolUse</code> only record what already happened.</p>
<h3 id="the-kill-switch-continue-false">The kill switch: <code>continue: false</code></h3>
<p>Any hook can return JSON with <code>"continue": false</code>. The reference says this "stops Claude entirely," and <code>stopReason</code> explains why (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>). It's the most reliable way for an unattended run to stop itself.</p>
<h3 id="posttoolbatch-the-per-step-budget-checkpoint"><code>PostToolBatch</code>: the per-step budget checkpoint</h3>
<p><code>PostToolBatch</code> fires after a whole batch of tool calls resolves and before Claude makes its next model call (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>). That makes it a good place to count steps. For example, a script could keep a counter file in the session's <code>scratchpad_dir</code> (an input field available from v2.1.257; see the <a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a> and <a href="https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md" rel="noopener">Claude Code changelog</a>). Once the count passes a limit you choose, it returns:</p>
<pre data-lang="json"><code class="language-json">{ &quot;continue&quot;: false, &quot;stopReason&quot;: &quot;Step budget of 80 tool batches reached. Stopping for review.&quot; }</code></pre>
<p>The number 80 is only an example. Choose a limit based on how many steps your normal runs take.</p>
<h3 id="pretooluse-block-the-expensive-call-itself"><code>PreToolUse</code>: block the expensive call itself</h3>
<p><code>PreToolUse</code> returns <code>permissionDecision</code> with the value <code>allow</code>, <code>deny</code>, <code>ask</code> or <code>defer</code>. It can also rewrite the call with <code>updatedInput</code> (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>). Use a matcher such as <code>Bash</code> or <code>mcp__.<em></code> and the <code>if</code> field (for example <code>"Bash(git </em>)"</code>) so the hook only runs on the calls you care about. One limitation: <code>deny</code> blocks that one call, and Claude may try a different approach afterwards. Pair it with a counter if you want the run to end.</p>
<h3 id="stop-useful-but-it-can-add-usage"><code>Stop</code>: useful, but it can <em>add</em> usage</h3>
<p>When a <code>Stop</code> hook exits with code 2, Claude doesn't stop and the conversation keeps going (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>). That's helpful for checks like "tests must pass before you're done." It's also the most common cause of loops that burn usage. Your hook should check the <code>stop_hook_active</code> input field, which is <code>true</code> when Claude is already continuing because a Stop hook blocked it. If it's true, exit 0 (<a href="https://amitkoth.com/claude-code-stop-hooks/" rel="noopener">Amit Kothari</a>, <a href="https://claudefa.st/blog/tools/hooks/stop-hook-task-enforcement" rel="noopener">claudefa.st</a>). A <a href="https://github.com/anthropics/claude-code/issues/54360" rel="noopener">GitHub issue (#54360)</a> reports cases where the flag didn't propagate, so add a hard cap as well. For example, allow at most 3 blocks per session.</p>
<h3 id="precompact-an-early-warning-about-long-runs"><code>PreCompact</code>: an early warning about long runs</h3>
<p><code>PreCompact</code> with the <code>auto</code> matcher fires before automatic compaction and can block (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>). If a routine job hits auto-compaction, it has probably grown larger than you meant it to. Logging it is a cheap signal.</p>
<h2 id="which-hooks-catch-silent-failures">Which hooks catch silent failures</h2>
<p>Silent failures show up in four events: <code>PostToolUseFailure</code>, <code>StopFailure</code>, <code>Notification</code> and <code>SessionEnd</code>. None of them can block. Their job is to tell you something went wrong.</p>
<ul><li><strong><code>StopFailure</code></strong> fires when a turn ends because of an API error. Its matcher accepts values like <code>rate_limit</code>, <code>overloaded</code>, <code>authentication_failed</code>, <code>billing_error</code> and <code>max_output_tokens</code> (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>). Send yourself a message on these. Without it, a cron run that hit a rate limit looks the same as one that completed. For more on why runs stop, see /why-claude-code-stops-mid-automation-and-how-to-fix-it/.</li><li><strong><code>PostToolUseFailure</code></strong> fires after any failed tool call. Write those failures to a file so you can check them in the morning.</li><li><strong><code>Notification</code></strong> with <code>idle_prompt</code> or <code>permission_prompt</code> tells you the run is stuck waiting for a human (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>).</li><li><strong><code>SessionEnd</code></strong> reports the reason the session ended (<code>clear</code>, <code>logout</code>, <code>prompt_input_exit</code>, <code>other</code>). Keep it quick because of the 1.5-second default budget.</li></ul>
<p>Hook failures can be silent too. If a command hook exits with code 1 or 3+, that's a non-blocking error: only the first line of stderr appears as a notice (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>). From v2.1.295 you can set <code>"onFailure": "block"</code> so a broken safety hook stops the run instead of letting it continue unchecked (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>, <a href="https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md" rel="noopener">changelog</a>).</p>
<h2 id="set-up-your-first-lifecycle-hook">Set up your first lifecycle hook</h2>
<p>Start with a project-level <code>Stop</code> guard plus a <code>StopFailure</code> logger. Together they cover both runaway loops and quiet failures.</p>
<ol><li><strong>Choose the file.</strong> Use <code>.claude/settings.json</code> to share hooks with your repo, <code>.claude/settings.local.json</code> for hooks only you use (gitignored), or <code>~/.claude/settings.json</code> for all your projects (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>).</li><li><strong>Add a <code>hooks</code> block.</strong> Example:</li></ol>
<pre data-lang="json"><code class="language-json">{
  &quot;hooks&quot;: {
    &quot;Stop&quot;: [
      { &quot;hooks&quot;: [ { &quot;type&quot;: &quot;command&quot;,
        &quot;command&quot;: &quot;${CLAUDE_PROJECT_DIR}/.claude/hooks/stop-guard.sh&quot; } ] }
    ],
    &quot;StopFailure&quot;: [
      { &quot;matcher&quot;: &quot;rate_limit|billing_error|server_error&quot;,
        &quot;hooks&quot;: [ { &quot;type&quot;: &quot;command&quot;,
          &quot;command&quot;: &quot;${CLAUDE_PROJECT_DIR}/.claude/hooks/alert.sh&quot; } ] }
    ]
  }
}</code></pre>
<ol><li><strong>Write <code>stop-guard.sh</code>.</strong> It reads JSON from stdin. As the first thing it does, it exits 0 if <code>stop_hook_active</code> is true. After that it runs your check, such as tests, and exits 2 with a reason on stderr if the check fails.</li><li><strong>Make scripts executable</strong> with <code>chmod +x .claude/hooks/*.sh</code>.</li><li><strong>Verify.</strong> Type <code>/hooks</code> in Claude Code to open the read-only hook browser. Select <strong>All events</strong> to confirm each hook appears with its source (user, project, local, plugin, skill or subagent) (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>). The file watcher normally picks up direct edits to settings files without a restart.</li><li><strong>Test with a small prompt</strong> before you hand the hook to a scheduled run. To schedule it afterwards, see /claude-code-cron-job-setup-run-automations-overnight/.</li></ol>
<h2 id="common-mistakes-and-fixes">Common mistakes and fixes</h2>
<p>Most hook problems come from a short list of misunderstandings.</p>
<div class="table-wrap"><table><thead><tr><th>Mistake</th><th>Fix</th></tr></thead><tbody><tr><td>Stop hook loops forever</td><td>Check <code>stop_hook_active</code> first and add a hard block counter (<a href="https://github.com/anthropics/claude-code/issues/54360" rel="noopener">issue #54360</a>)</td></tr><tr><td>Hook exits 1 and nothing happens</td><td>Only exit 2 blocks. Codes 1 and 3+ are non-blocking errors (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>)</td></tr><tr><td>Matcher on <code>Stop</code> ignored</td><td><code>Stop</code>, <code>UserPromptSubmit</code> and <code>PostToolBatch</code> don't support matchers. They always fire (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>)</td></tr><tr><td>Path breaks with spaces</td><td>Use exec form (<code>command</code> + <code>args</code>) with <code>${CLAUDE_PROJECT_DIR}</code> (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>)</td></tr><tr><td>Huge stdout clutters context</td><td>Output over 10,000 characters is saved to a file with a preview, so keep it short (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>)</td></tr><tr><td>Expecting order between hooks</td><td>Matching hooks run in parallel. Put dependent steps in one script</td></tr></tbody></table></div>
<p>If you want to undo the <em>file edits</em> a run made, rather than prevent them, see /claude-code-checkpoints-how-rollback-works-in-automations/.</p>
<h2 id="next-steps">Next steps</h2>
<p>You can have useful guards in place today with three small changes.</p>
<p><strong>Do it yourself today:</strong></p>
<ol><li>Add the <code>Stop</code> guard and <code>StopFailure</code> alert from the steps above to one project.</li><li>Add a <code>PostToolBatch</code> step counter that returns <code>continue: false</code> at a limit you choose.</li><li>Run <code>/hooks</code>, select <strong>All events</strong>, and confirm what's actually active, including hooks from plugins.</li></ol>
<p><strong>If you'd rather not do it yourself:</strong> a paid plan doesn't write hooks for you, but it's required to run Claude Code. Claude Code is included in all paid plans and shares your plan's usage limits. Pro costs $20/month or $17/month billed annually, Max starts at $100/month, and Team Standard seats cost $25/month monthly or $20 annually (as of October 2026; check the <a href="https://claude.com/pricing" rel="noopener">official page</a>). Pro suits solo builders running a few jobs a day. It doesn't suit long nightly multi-agent runs, which can hit the limits. Max gives you more usage but doesn't fix a hook that loops. If you'd rather hire someone, a freelance consultant can set up hooks for you. Give them the <a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference</a> as the spec, and make sure you understand every script before it runs unattended.</p>
<p><strong>Related guides on this site:</strong></p>
<ul><li>/why-claude-code-stops-mid-automation-and-how-to-fix-it/</li><li>/claude-code-checkpoints-how-rollback-works-in-automations/</li><li>/claude-code-cron-job-setup-run-automations-overnight/</li></ul>
<h2 id="faq">FAQ</h2>
<h3 id="what-is-the-order-of-claude-code-hook-events">What is the order of Claude Code hook events?</h3>
<p>A typical run goes <code>SessionStart</code> → <code>UserPromptSubmit</code> → <code>PreToolUse</code> → <code>PostToolUse</code>/<code>PostToolUseFailure</code> → <code>PostToolBatch</code>, and the tool steps repeat until <code>Stop</code> or <code>StopFailure</code>. <code>SessionEnd</code> fires when the session closes. Compaction, subagent and notification events fire in between as needed (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>).</p>
<h3 id="which-claude-code-hook-can-block-an-action">Which Claude Code hook can block an action?</h3>
<p><code>PreToolUse</code>, <code>UserPromptSubmit</code>, <code>PostToolBatch</code>, <code>Stop</code>, <code>SubagentStop</code>, <code>PreCompact</code>, <code>ConfigChange</code> and several task, worktree and elicitation events can block with exit code 2 or a JSON decision. <code>SessionStart</code>, <code>Notification</code>, <code>StopFailure</code> and <code>SessionEnd</code> can't (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>).</p>
<h3 id="how-do-i-stop-a-claude-code-stop-hook-from-looping">How do I stop a Claude Code Stop hook from looping?</h3>
<p>Read <code>stop_hook_active</code> from the hook's stdin and exit 0 when it's <code>true</code>. Because of a reported propagation bug, also keep a per-session counter and allow the stop after a few blocks (<a href="https://github.com/anthropics/claude-code/issues/54360" rel="noopener">issue #54360</a>).</p>
<h3 id="where-do-i-put-claude-code-hooks">Where do I put Claude Code hooks?</h3>
<p>Put them in <code>~/.claude/settings.json</code> (all projects), <code>.claude/settings.json</code> (shared project), <code>.claude/settings.local.json</code> (personal project), plugin <code>hooks/hooks.json</code>, or skill and subagent frontmatter. Hooks from all of these locations run together (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>).</p>
<h3 id="do-prompt-or-agent-hooks-cost-extra">Do prompt or agent hooks cost extra?</h3>
<p>They use model calls. <code>prompt</code> hooks send text to a model, and <code>agent</code> hooks start a subagent (marked experimental), so they count toward your plan's usage (<a href="https://code.claude.com/docs/en/hooks" rel="noopener">reference</a>, <a href="https://claude.com/pricing" rel="noopener">pricing</a>). Use command hooks for checks that don't need judgment.</p>
<h2 id="sources">Sources</h2>
<ul><li><a href="https://code.claude.com/docs/en/hooks" rel="noopener">Hooks reference – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener">Automate actions with hooks – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/agent-sdk/hooks" rel="noopener">Intercept and control agent behavior with hooks – Agent SDK Docs</a></li><li><a href="https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md" rel="noopener">Claude Code changelog – GitHub</a></li><li><a href="https://claude.com/pricing" rel="noopener">Claude pricing</a></li><li><a href="https://github.com/anthropics/claude-code/issues/54360" rel="noopener">GitHub issue #54360: Stop hook fires repeatedly (stop_hook_active not propagating)</a></li><li><a href="https://amitkoth.com/claude-code-stop-hooks/" rel="noopener">How Claude Code stop hooks work – Amit Kothari</a></li><li><a href="https://claudefa.st/blog/tools/hooks/stop-hook-task-enforcement" rel="noopener">Claude Code Stop Hook: Force Task Completion – claudefa.st</a></li></ul><p>Originally published at <a href="https://en.aobaworks.com/claude-code-hooks-lifecycle-every-event-explained/">Unattended</a>.</p>]]></content:encoded><enclosure url="https://en.aobaworks.com/og/claude-code-hooks-lifecycle-every-event-explained.png" type="image/png" length="685694"/><media:content url="https://en.aobaworks.com/og/claude-code-hooks-lifecycle-every-event-explained.png" medium="image" type="image/png" width="1200" height="630"/></item><item><title>Claude Code Cron Job Setup: Run Automations Overnight</title><link>https://en.aobaworks.com/claude-code-cron-job-setup-run-automations-overnight/</link><guid isPermaLink="true">https://en.aobaworks.com/claude-code-cron-job-setup-run-automations-overnight/</guid><pubDate>Thu, 15 Oct 2026 03:00:00 GMT</pubDate><dc:creator>Aoba Works</dc:creator><description>Claude Code cron job setup: auth tokens, PATH, working directory, permissions, locks and log rotation, so overnight claude -p runs don't fail silently.</description><category>claude code</category><category>cron</category><category>automation</category><category>headless</category><category>claude -p</category><category>devops</category><content:encoded><![CDATA[<figure><img src="https://en.aobaworks.com/og/claude-code-cron-job-setup-run-automations-overnight.png" alt="Claude Code Cron Job Setup: Run Automations Overnight" width="1200" height="630"></figure><p class="meta ai-note">How this guide was made: drafted with AI assistance from the official sources linked below, then passed an automated editorial check before publishing. <a href="https://en.aobaworks.com/about/#how-guides-are-made">Our process</a></p><p>To set up a Claude Code cron job, you need two pieces. The first is a wrapper script that sets <code>PATH</code>, loads a token, <code>cd</code>s into your project and runs <code>claude -p</code> with explicit permissions and limits. The second is one crontab line that runs that script under a lock and sends its output to a log file. Most "silent" overnight failures come from what cron does <em>not</em> give you: your shell's environment, your login, your project directory and a place to see errors. This guide covers each of those gaps, plus the scheduling options Anthropic offers if you don't want to manage cron yourself.</p>
<h2 id="should-you-use-cron-or-a-built-in-claude-code-scheduler">Should you use cron, or a built-in Claude Code scheduler?</h2>
<p>Use cron when the job needs your machine's local files and tools and you want full control. Use Anthropic's built-in options when you'd rather not maintain a server. Claude Code now has three of its own scheduling options, and the <a href="https://code.claude.com/docs/en/scheduled-tasks" rel="noopener">official comparison</a> shows how they differ:</p>
<div class="table-wrap"><table><thead><tr><th>Option</th><th>Runs on</th><th>Machine must be on</th><th>Local files</th><th>Minimum interval</th><th>Main limitation</th></tr></thead><tbody><tr><td>System cron + <code>claude -p</code></td><td>Your machine/server</td><td>Yes</td><td>Yes</td><td>1 minute (cron)</td><td>You handle auth, env, logs, locking</td></tr><tr><td><a href="https://code.claude.com/docs/en/routines" rel="noopener">Routines</a> (cloud)</td><td>Anthropic cloud</td><td>No</td><td>No (fresh clone)</td><td>1 hour</td><td>Research preview; GitHub repos only</td></tr><tr><td><a href="https://code.claude.com/docs/en/desktop-scheduled-tasks" rel="noopener">Desktop scheduled tasks</a></td><td>Your machine</td><td>Yes, app open and awake</td><td>Yes</td><td>1 minute</td><td>Skipped while the computer sleeps</td></tr><tr><td><a href="https://code.claude.com/docs/en/scheduled-tasks" rel="noopener"><code>/loop</code></a></td><td>Your open session</td><td>Yes</td><td>Yes</td><td>1 minute</td><td>Session-scoped, expires after 7 days</td></tr></tbody></table></div>
<p>How to choose:</p>
<ul><li><strong>Routines</strong> suit you if your work lives in GitHub and should run while your laptop is closed. They're available on Pro, Max, Team and Enterprise plans (<a href="https://code.claude.com/docs/en/routines" rel="noopener">source</a>, as of October 2026). <strong>Not for you if</strong> the job needs files outside a repo, local databases, or tools you can't install from a setup script. The docs also note that routines are in research preview and that a green run status "does not mean the task in your prompt succeeded" (<a href="https://code.claude.com/docs/en/routines" rel="noopener">source</a>).</li><li><strong>Desktop scheduled tasks</strong> suit people who keep the Claude Desktop app open on a work machine. <strong>Not for you if</strong> the machine sleeps overnight. If it sleeps through a scheduled time, the run is skipped, and on wake only one catch-up run happens (<a href="https://code.claude.com/docs/en/desktop-scheduled-tasks" rel="noopener">source</a>).</li><li><strong><code>/loop</code></strong> is for polling during a session you have open. Recurring tasks expire seven days after you create them (<a href="https://code.claude.com/docs/en/scheduled-tasks" rel="noopener">source</a>), so it can't replace cron.</li><li><strong>Cron</strong> fits an always-on Linux box, VPS or home server where you want plain files, plain logs and no extra platform.</li></ul>
<p>The rest of this guide is about the cron route.</p>
<h2 id="trap-1-authentication-that-works-in-your-terminal-but-not-in-cron">Trap 1: authentication that works in your terminal but not in cron</h2>
<p>Your cron job needs a credential passed in explicitly through an environment variable, because the login in your interactive terminal may not reach it. Claude Code's <a href="https://code.claude.com/docs/en/authentication" rel="noopener">authentication docs</a> list two common options for scripts:</p>
<ul><li><strong>Subscription token:</strong> run <code>claude setup-token</code> once. It opens the browser flow and prints a one-year OAuth token without saving it anywhere. Set that token as <code>CLAUDE_CODE_OAUTH_TOKEN</code>. It requires a Pro, Max, Team or Enterprise plan (<a href="https://code.claude.com/docs/en/authentication" rel="noopener">source</a>, as of October 2026), and it can only make model requests, so claude.ai connectors won't work with it.</li><li><strong>API key:</strong> set <code>ANTHROPIC_API_KEY</code> to a key from the Claude Console. Usage is billed to your API account instead of your subscription. In <code>-p</code> mode, the key "is always used when present" (<a href="https://code.claude.com/docs/en/authentication" rel="noopener">source</a>).</li></ul>
<p>Two details often cause trouble:</p>
<ol><li><strong>Precedence.</strong> <code>ANTHROPIC_API_KEY</code> ranks above <code>CLAUDE_CODE_OAUTH_TOKEN</code> (<a href="https://code.claude.com/docs/en/authentication" rel="noopener">source</a>). If both exist, you are billed through the API key, which may not be what you intended.</li><li><strong><code>--bare</code> ignores the OAuth token.</strong> Bare mode is recommended for scripts, but it "does not read <code>CLAUDE_CODE_OAUTH_TOKEN</code>". With <code>--bare</code>, use <code>ANTHROPIC_API_KEY</code> or an <code>apiKeyHelper</code> instead (<a href="https://code.claude.com/docs/en/headless" rel="noopener">source</a>).</li></ol>
<p>Why this fails silently: when a failure happens inside the run, "such as missing authentication, Claude Code prints the failure as the result on stdout" (<a href="https://code.claude.com/docs/en/headless" rel="noopener">source</a>). If you throw stdout away, the error disappears with it. Check the exit code too: Claude Code exits with a non-zero code when a run fails.</p>
<h2 id="trap-2-path-and-the-minimal-cron-environment">Trap 2: PATH and the minimal cron environment</h2>
<p>Cron runs your command in a much smaller environment than your terminal, so set every variable the job needs inside the script itself. The <a href="https://man7.org/linux/man-pages/man5/crontab.5.html" rel="noopener">crontab(5) man page</a> says cron sets <code>SHELL</code> to <code>/bin/sh</code> and takes <code>LOGNAME</code> and <code>HOME</code> from <code>/etc/passwd</code>. Anything your <code>~/.zshrc</code> or <code>~/.bashrc</code> adds is not guaranteed to be there.</p>
<p>That matters for Claude Code because the native installer puts the launcher at <code>~/.local/bin/claude</code> (<a href="https://code.claude.com/docs/en/setup" rel="noopener">source</a>), and that directory is often added to <code>PATH</code> only by your shell profile. The tools Claude calls during the run also need to be found: <code>node</code>, <code>git</code>, <code>gh</code>, <code>npm</code> and so on.</p>
<p>Practical fixes:</p>
<ul><li>Use the absolute path to <code>claude</code> (run <code>which claude</code> in your terminal to find it), or export a full <code>PATH</code> at the top of the script.</li><li>Keep secrets in a dedicated env file with <code>chmod 600</code>, and load it in the script. Don't paste them into the crontab.</li><li>Watch for <code>%</code> in crontab lines. Unless escaped, cron turns it into a newline and sends the rest of the line to standard input (<a href="https://man7.org/linux/man-pages/man5/crontab.5.html" rel="noopener">source</a>). That breaks <code>date +%F</code> inline, so put date formatting in the script.</li><li>Cron uses the system timezone unless you set <code>CRON_TZ</code> (on cron implementations that support it, such as cronie, per the <a href="https://man7.org/linux/man-pages/man5/crontab.5.html" rel="noopener">crontab(5) man page</a>).</li></ul>
<h2 id="trap-3-the-working-directory-decides-what-claude-loads">Trap 3: the working directory decides what Claude loads</h2>
<p>Always <code>cd</code> into the project explicitly, because the folder a <code>-p</code> run starts in decides which configuration and instructions it loads. Without <code>--bare</code>, <code>claude -p</code> "loads the same context an interactive session would, including anything configured in the working directory or <code>~/.claude</code>". That includes <code>CLAUDE.md</code>, the hooks in <code>.claude/settings.json</code> and the MCP servers in <code>.mcp.json</code> (<a href="https://code.claude.com/docs/en/headless" rel="noopener">source</a>).</p>
<p>Two consequences:</p>
<ul><li><strong>Wrong directory, wrong behavior.</strong> If the job starts somewhere else, Claude won't see your project's <code>CLAUDE.md</code> or settings, and relative paths in your prompt point to the wrong files.</li><li><strong>No trust dialog.</strong> A <code>-p</code> session "shows no workspace trust dialog and no per-server approval prompt", so project hooks and MCP servers run even in a folder you've never trusted (<a href="https://code.claude.com/docs/en/headless" rel="noopener">source</a>). Only point cron at repositories you control.</li></ul>
<p>If you want the same result on every run no matter what's in <code>~/.claude</code>, use <code>--bare</code> and pass context explicitly with <code>--append-system-prompt-file</code>, <code>--settings</code> and <code>--mcp-config</code> (<a href="https://code.claude.com/docs/en/headless" rel="noopener">source</a>). The trade-off is that you then need an API key (see Trap 1).</p>
<h2 id="trap-4-permissions-in-a-run-nobody-is-watching">Trap 4: permissions in a run nobody is watching</h2>
<p>Set the permission mode and the allowed tools explicitly, because no one is awake to answer a prompt. The docs warn that a run with no permission mode set takes a built-in starting mode, "which can be <code>auto</code>, so pass the one you want" (<a href="https://code.claude.com/docs/en/headless" rel="noopener">source</a>).</p>
<p>Options from the <a href="https://code.claude.com/docs/en/headless" rel="noopener">headless docs</a> and the <a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference</a>:</p>
<ul><li><code>--permission-mode dontAsk</code> denies anything that would prompt, while reads and your <code>--allowedTools</code> entries still run. This is a good default for locked-down jobs.</li><li><code>--allowedTools "Read,Edit,Bash(npm test <em>)"</code> grants narrow, prefix-matched commands. The space before <code></em></code> matters: <code>Bash(git diff*)</code> would also match <code>git diff-index</code>.</li><li><code>--permission-prompts none</code> tells Claude that nobody can approve requests and not to retry denied ones. It requires v2.1.259 or later.</li><li><code>--max-turns</code> caps agentic turns. There is no limit by default.</li><li><code>--max-budget-usd</code> stops the run at an estimated spend. It's a client-side estimate, and spend can go past the cap, so leave headroom.</li></ul>
<p>Avoid <code>--dangerously-skip-permissions</code> for overnight jobs unless the machine is a disposable sandbox.</p>
<h2 id="putting-it-together-script-crontab-lock-and-log-rotation">Putting it together: script, crontab, lock and log rotation</h2>
<p>The setup has four parts: a wrapper script, a crontab line with <code>flock</code>, structured logs, and rotation so the logs don't fill your disk. Everything below is <strong>an example</strong>. Change the paths, prompt and tool list to fit your project.</p>
<h3 id="step-1-the-wrapper-script">Step 1: the wrapper script</h3>
<pre data-lang="bash"><code class="language-bash">#!/usr/bin/env bash
# ~/bin/claude-nightly.sh  (example)
set -uo pipefail

export PATH=&quot;$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin&quot;
set -a; source &quot;$HOME/.config/claude-cron/env&quot;; set +a   # holds CLAUDE_CODE_OAUTH_TOKEN=...
unset ANTHROPIC_API_KEY   # avoid accidental precedence over the token

cd &quot;$HOME/projects/myapp&quot; || { echo &quot;cd failed&quot;; exit 1; }

LOG_DIR=&quot;$HOME/logs/claude-cron&quot;
mkdir -p &quot;$LOG_DIR&quot;
STAMP=$(date +%Y-%m-%dT%H%M)
OUT=&quot;$LOG_DIR/run-$STAMP.json&quot;

claude -p &quot;$(cat .claude/nightly-prompt.md)&quot; \
  --permission-mode dontAsk \
  --permission-prompts none \
  --allowedTools &quot;Read,Edit,Bash(npm test *),Bash(git status *),Bash(git diff *)&quot; \
  --max-turns 30 \
  --max-budget-usd 3 \
  --output-format json &gt; &quot;$OUT&quot; 2&gt; &quot;$LOG_DIR/run-$STAMP.err&quot;
STATUS=$?

echo &quot;$(date -Is) exit=$STATUS cost=$(jq -r &#x27;.total_cost_usd // &quot;n/a&quot;&#x27; &quot;$OUT&quot; 2&gt;/dev/null)&quot;
[ &quot;$STATUS&quot; -eq 0 ] || echo &quot;FAILED: $(jq -r &#x27;.result // empty&#x27; &quot;$OUT&quot; 2&gt;/dev/null | head -c 500)&quot;
exit &quot;$STATUS&quot;</code></pre>
<p>Why these choices: <code>--output-format json</code> gives you <code>result</code>, <code>session_id</code> and <code>total_cost_usd</code> in a form a script can parse (<a href="https://code.claude.com/docs/en/headless" rel="noopener">source</a>). Branching on the exit code catches failed runs. Keeping the prompt in a file means you can version-control it.</p>
<h3 id="step-2-the-crontab-line">Step 2: the crontab line</h3>
<p>Run <code>crontab -e</code>:</p>
<pre data-lang="cron"><code class="language-cron">SHELL=/bin/bash
MAILTO=&quot;&quot;
7 2 * * * /usr/bin/flock -n /tmp/claude-nightly.lock $HOME/bin/claude-nightly.sh &gt;&gt; $HOME/logs/claude-cron/cron.log 2&gt;&amp;1</code></pre>
<p><code>flock -n</code> makes the job "fail rather than wait if the lock cannot be immediately acquired" (<a href="https://man7.org/linux/man-pages/man1/flock.1.html" rel="noopener">flock(1)</a>). If last night's run is still going, a second copy won't start on top of it. Cron mails command output to the crontab owner or to <code>MAILTO</code> (<a href="https://manpages.debian.org/bookworm/cron/cron.8.en.html" rel="noopener">cron(8)</a>). Since this setup sends output to a file, set <code>MAILTO=""</code> so cron doesn't also try to send mail.</p>
<h3 id="step-3-log-rotation">Step 3: log rotation</h3>
<p>Rotate the summary log with logrotate, and prune the per-run files with <code>find</code>. An example <code>/etc/logrotate.d/claude-cron</code>:</p>
<pre><code>/home/you/logs/claude-cron/cron.log {
    weekly
    rotate 8
    compress
    missingok
    notifempty
    copytruncate
}</code></pre>
<p><code>copytruncate</code> truncates the file in place after copying it, so the <code>&gt;&gt;</code> in your crontab keeps writing to the same file. <code>missingok</code> and <code>notifempty</code> skip missing or empty logs (<a href="https://man7.org/linux/man-pages/man8/logrotate.8.html" rel="noopener">logrotate(8)</a>). For the per-run JSON files, add a second cron line such as <code>0 4 <em> </em> 0 find $HOME/logs/claude-cron -name 'run-*' -mtime +30 -delete</code>.</p>
<p>Claude Code also saves session transcripts on disk. If you never resume these runs, <code>--no-session-persistence</code> stops them from being saved (<a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">source</a>).</p>
<h3 id="step-4-test-it-under-cron-conditions">Step 4: test it under cron conditions</h3>
<p>Before you trust the schedule, run the script in an environment as bare as cron's: <code>env -i HOME="$HOME" /bin/sh -c '$HOME/bin/claude-nightly.sh'</code>. Then schedule it a few minutes ahead and read <code>cron.log</code> and the JSON file. Also run <code>claude auth status</code> inside the script once: it reports which <code>authMethod</code> is active and exits 1 if you're not logged in (<a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">source</a>).</p>
<h2 id="keeping-overnight-jobs-healthy-over-time">Keeping overnight jobs healthy over time</h2>
<p>Plan for updates, token expiry and alerting from the start, because these cause failures weeks after setup rather than on day one.</p>
<ul><li><strong>Updates:</strong> native installs update automatically in the background (<a href="https://code.claude.com/docs/en/setup" rel="noopener">source</a>). If you want a predictable version for scheduled jobs, set <code>"autoUpdatesChannel": "stable"</code>, or set <code>DISABLE_AUTOUPDATER=1</code> in your settings and update yourself.</li><li><strong>Token expiry:</strong> the <code>setup-token</code> token lasts one year (<a href="https://code.claude.com/docs/en/authentication" rel="noopener">source</a>). Put a renewal reminder in your calendar.</li><li><strong>Alerting:</strong> a failing exit code that only reaches a log file is still silent. For example, you could add a line to the script that sends the <code>FAILED:</code> message to a Slack webhook or a health-check service when the run fails.</li><li><strong>Read the output, not just the status:</strong> as with routines, a clean exit means the process finished, not that the task was done well. Check the <code>result</code> text now and then.</li></ul>
<h2 id="faq">FAQ</h2>
<h3 id="can-claude-code-run-on-a-schedule-without-my-computer-on">Can Claude Code run on a schedule without my computer on?</h3>
<p>Yes, through Routines, which run on Anthropic-managed cloud infrastructure on hourly, nightly or weekly schedules (<a href="https://code.claude.com/docs/en/routines" rel="noopener">source</a>). They work from a fresh clone of your GitHub repositories, so they can't see local files. The minimum interval is one hour, and the feature is in research preview.</p>
<h3 id="why-does-my-claude-code-cron-job-work-manually-but-not-from-cron">Why does my Claude Code cron job work manually but not from cron?</h3>
<p>Usually cron's environment is missing something your terminal has: <code>~/.local/bin</code> on <code>PATH</code>, an exported token, or the project directory. Use absolute paths, load credentials from a file, and <code>cd</code> explicitly. Capture stdout, because Claude Code prints failures such as missing authentication there (<a href="https://code.claude.com/docs/en/headless" rel="noopener">source</a>).</p>
<h3 id="should-i-use-claude-code-oauth-token-or-anthropic-api-key-for-cron">Should I use CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY for cron?</h3>
<p>Use <code>CLAUDE_CODE_OAUTH_TOKEN</code> (from <code>claude setup-token</code>) if you want runs to count against your Claude subscription. Use <code>ANTHROPIC_API_KEY</code> if you want API billing, or if you run with <code>--bare</code>, which ignores the OAuth token (<a href="https://code.claude.com/docs/en/authentication" rel="noopener">source</a>). Don't set both, because the API key takes precedence.</p>
<h3 id="how-do-i-stop-a-scheduled-claude-run-from-costing-too-much">How do I stop a scheduled Claude run from costing too much?</h3>
<p>Combine <code>--max-turns</code> with <code>--max-budget-usd</code>, and log <code>total_cost_usd</code> from the JSON output. The budget cap uses a client-side estimate and spend can go past it, so set it below your real limit (<a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">source</a>).</p>
<h3 id="how-do-i-prevent-overlapping-claude-cron-runs">How do I prevent overlapping Claude cron runs?</h3>
<p>Wrap the command in <code>flock -n</code> with a lock file. A new run then exits right away if the previous one still holds the lock (<a href="https://man7.org/linux/man-pages/man1/flock.1.html" rel="noopener">flock(1)</a>).</p>
<h2 id="sources">Sources</h2>
<ul><li><a href="https://code.claude.com/docs/en/headless" rel="noopener">Run Claude Code programmatically (headless) – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/authentication" rel="noopener">Authentication – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/setup" rel="noopener">Advanced setup – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/scheduled-tasks" rel="noopener">Run prompts on a schedule – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/routines" rel="noopener">Automate work with routines – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/desktop-scheduled-tasks" rel="noopener">Schedule recurring tasks in Claude Code Desktop – Claude Code Docs</a></li><li><a href="https://man7.org/linux/man-pages/man5/crontab.5.html" rel="noopener">crontab(5) – Linux manual page</a></li><li><a href="https://manpages.debian.org/bookworm/cron/cron.8.en.html" rel="noopener">cron(8) – Debian manpages</a></li><li><a href="https://man7.org/linux/man-pages/man1/flock.1.html" rel="noopener">flock(1) – Linux manual page</a></li><li><a href="https://man7.org/linux/man-pages/man8/logrotate.8.html" rel="noopener">logrotate(8) – Linux manual page</a></li></ul><p>Originally published at <a href="https://en.aobaworks.com/claude-code-cron-job-setup-run-automations-overnight/">Unattended</a>.</p>]]></content:encoded><enclosure url="https://en.aobaworks.com/og/claude-code-cron-job-setup-run-automations-overnight.png" type="image/png" length="642977"/><media:content url="https://en.aobaworks.com/og/claude-code-cron-job-setup-run-automations-overnight.png" medium="image" type="image/png" width="1200" height="630"/></item><item><title>Claude Code Checkpoints: How Rollback Works in Automations</title><link>https://en.aobaworks.com/claude-code-checkpoints-how-rollback-works-in-automations/</link><guid isPermaLink="true">https://en.aobaworks.com/claude-code-checkpoints-how-rollback-works-in-automations/</guid><pubDate>Wed, 14 Oct 2026 03:00:00 GMT</pubDate><dc:creator>Aoba Works</dc:creator><description>How Claude Code checkpoints work in headless jobs: enable SDK file checkpointing, save UUIDs, rewind unattended runs, and learn what rewind can't undo.</description><category>claude code</category><category>checkpoints</category><category>rollback</category><category>headless</category><category>agent sdk</category><category>automation</category><content:encoded><![CDATA[<figure><img src="https://en.aobaworks.com/og/claude-code-checkpoints-how-rollback-works-in-automations.png" alt="Claude Code Checkpoints: How Rollback Works in Automations" width="1200" height="630"></figure><p class="meta ai-note">How this guide was made: drafted with AI assistance from the official sources linked below, then passed an automated editorial check before publishing. <a href="https://en.aobaworks.com/about/#how-guides-are-made">Our process</a></p><p>In interactive Claude Code, checkpoints are on by default, and you undo with <code>/rewind</code> or by pressing <code>Esc</code> twice when the prompt is empty. Headless runs (<code>claude -p</code> or the Agent SDK) have nobody to press that key. You have to turn file checkpointing on, save the first user-message UUID and the session ID, and have your script call <code>rewindFiles()</code> / <code>rewind_files()</code> or run <code>claude -p --resume &lt;session-id&gt; --rewind-files &lt;checkpoint-uuid&gt;</code>. Rewind only restores edits made with Write, Edit and NotebookEdit. It doesn't cover Bash, most subagent edits or directories, so git stays your main safety net.</p>
<h2 id="which-option-is-right-for-you">Which option is right for you?</h2>
<p>Your setup depends on what the job does: rewind fully covers only jobs that edit files through Claude's own edit tools.</p>
<div class="table-wrap"><table><thead><tr><th>Your unattended job mostly…</th><th>Main safety net</th><th>Role of checkpoint rewind</th></tr></thead><tbody><tr><td><strong>Edits files</strong> (refactors, doc updates, lint fixes made with Edit/Write)</td><td>Rewind, with a git commit underneath</td><td>Primary undo. Fast, and it can go back to a middle turn</td></tr><tr><td><strong>Runs shell commands that change files</strong> (formatters, codemods, <code>npm install</code>, <code>sed -i</code>, <code>mv</code>)</td><td>git, or a worktree you can throw away</td><td>Partial at best. Bash changes are not tracked</td></tr><tr><td><strong>Uses subagents or background forked skills</strong></td><td>git, or a worktree</td><td>Assume it won't restore their edits</td></tr><tr><td><strong>Touches things outside the repo</strong> (APIs, databases, deploys)</td><td>Permission limits and dry runs</td><td>None. Rewind only covers local files</td></tr></tbody></table></div>
<p>If you're not sure, start every run on a clean git commit or in a worktree, and treat rewind as a convenience on top.</p>
<p>A detail people often miss: <code>--permission-mode acceptEdits</code> also auto-approves common filesystem commands such as <code>mkdir</code>, <code>touch</code>, <code>mv</code> and <code>cp</code> (<a href="https://code.claude.com/docs/en/headless" rel="noopener">Headless docs</a>). Those run through Bash, so rewind can't undo them even in a job you thought "only edits files."</p>
<h2 id="what-a-checkpoint-is-and-what-it-isn-t">What a checkpoint is and what it isn't</h2>
<p>A checkpoint is a snapshot of files Claude edited with its file tools. It is not a full backup of your working tree. In interactive mode, Claude Code takes one each time you send a prompt that starts a turn. It keeps file snapshots for the 100 most recent checkpoints in a session and saves them with the conversation, so <code>/rewind</code> still works after you resume (<a href="https://code.claude.com/docs/en/checkpointing" rel="noopener">Checkpointing docs</a>). The rewind menu can restore code and conversation, only the conversation, or only the code. It can also summarize from or up to a chosen point (<a href="https://code.claude.com/docs/en/checkpointing" rel="noopener">same source</a>).</p>
<p>These limits matter more when nobody is watching:</p>
<ul><li><strong>Bash changes are not tracked.</strong> Rewind can't undo <code>rm</code>, <code>mv</code>, <code>cp</code>, <code>sed -i</code> or <code>echo &gt; file</code> when they run through Bash (<a href="https://code.claude.com/docs/en/checkpointing" rel="noopener">Checkpointing docs</a>; <a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>).</li><li><strong>Subagent edits usually aren't restored.</strong> The only exception is a skill with <code>context: fork</code> that runs in the foreground. Forked skills run in the background by default, and you set <code>background: false</code> to run one in the foreground. For every other subagent, the docs say to revert with git (<a href="https://code.claude.com/docs/en/checkpointing" rel="noopener">Checkpointing docs</a>).</li><li><strong>Directories aren't undone.</strong> Rewind deletes files Claude created and restores files it modified. It doesn't reverse creating, moving or deleting directories, and it doesn't track remote or network files (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>).</li><li><strong>Links and some other paths are skipped.</strong> Since v2.1.216, rewind skips symlinks, hard links, other non-regular files, files whose parent directory no longer resolves to where it was at checkpoint time, and files whose backup can't be read safely. <code>RewindFilesResult.skippedLinks</code> counts all of these. Before v2.1.216, rewind wrote and deleted through links (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>).</li><li><strong>Snapshots expire.</strong> By default, the retention sweep deletes a session's snapshots about 30 days after the session last saved one. After that, a rewind can fail with "No files were restored." To keep snapshots longer, set <code>cleanupPeriodDays</code> (<a href="https://code.claude.com/docs/en/checkpointing" rel="noopener">Checkpointing docs</a>; <a href="https://code.claude.com/docs/en/settings-reference" rel="noopener">Settings reference</a>).</li><li><strong>Checkpoints don't replace version control.</strong> Anthropic describes them as "quick, session-level recovery" and recommends git for permanent history (<a href="https://code.claude.com/docs/en/checkpointing" rel="noopener">Checkpointing docs</a>).</li></ul>
<p>The <code>fileCheckpointingEnabled</code> setting turns the interactive <code>/rewind</code> snapshots on or off. When it is unset, snapshots are on (<a href="https://code.claude.com/docs/en/settings-reference" rel="noopener">Settings reference</a>).</p>
<h2 id="how-headless-checkpointing-differs">How headless checkpointing differs</h2>
<p>In the Agent SDK, checkpointing is opt-in, and it takes two options (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>):</p>
<div class="table-wrap"><table><thead><tr><th>Purpose</th><th>Python</th><th>TypeScript</th></tr></thead><tbody><tr><td>Track file changes</td><td><code>enable_file_checkpointing=True</code></td><td><code>enableFileCheckpointing: true</code></td></tr><tr><td>Get checkpoint UUIDs in the stream</td><td><code>extra_args={"replay-user-messages": None}</code></td><td><code>extraArgs: { "replay-user-messages": null }</code></td></tr></tbody></table></div>
<p>With both set, each user message in the response stream carries a <code>uuid</code> you can use as a restore point. Rewinding to the <strong>first</strong> user message's UUID returns tracked files to their original state. Rewinding files doesn't rewind the conversation, which stays as it was (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>).</p>
<p>The official examples also set <code>permission_mode="acceptEdits"</code> so edits go through without a prompt (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>). Always pass a permission mode explicitly. A <code>-p</code> run that sets none starts in a built-in default mode, and that default can be <code>auto</code> (<a href="https://code.claude.com/docs/en/headless" rel="noopener">Headless docs</a>).</p>
<h2 id="pattern-1-rewind-inside-the-run-when-validation-fails">Pattern 1: Rewind inside the run when validation fails</h2>
<p>Use this pattern when your script can tell during the run that something has gone wrong. Examples: a test command fails, a protected file changes, or the diff is much larger than expected.</p>
<p>The docs' "checkpoint before risky operations" pattern keeps a user-message UUID and calls <code>rewind_files()</code> as soon as your own condition is true, while the stream is still open (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>). Here is a shortened Python version. <code>should_revert()</code> is a placeholder for your own check:</p>
<pre data-lang="python"><code class="language-python">options = ClaudeAgentOptions(
    enable_file_checkpointing=True,
    permission_mode=&quot;acceptEdits&quot;,
    extra_args={&quot;replay-user-messages&quot;: None},
)

first_checkpoint = None
async with ClaudeSDKClient(options) as client:
    await client.query(&quot;Update dependencies and fix type errors&quot;)
    async for message in client.receive_response():
        if isinstance(message, UserMessage) and message.uuid and not first_checkpoint:
            first_checkpoint = message.uuid
        if should_revert(message) and first_checkpoint:   # your own check
            await client.rewind_files(first_checkpoint)
            break</code></pre>
<p>You need to make two decisions:</p>
<ol><li><strong>Pick which checkpoint to rewind to.</strong> The docs' version overwrites its variable every turn, so it keeps only the latest checkpoint and undoes only the last step. An unattended job usually wants to throw away the whole attempt, so the example above keeps the first UUID. To keep some turns and undo others, store every UUID. The docs call this "Multiple restore points" (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>).</li><li><strong>Call rewind before the loop ends.</strong> If you call <code>rewindFiles()</code> after you've finished iterating, you get "ProcessTransport is not ready for writing," because the connection closes when the loop completes (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>).</li></ol>
<h2 id="pattern-2-record-ids-now-rewind-later">Pattern 2: Record IDs now, rewind later</h2>
<p>Use this pattern when you only find out a run was bad after it ends, for example when CI goes red an hour later or someone reviews the diff the next morning.</p>
<ol><li><strong>Save <code>checkpoint_id</code> and <code>session_id</code> for every run.</strong> A small JSON log next to the job output is enough. The docs show how to capture <code>session_id</code> from the <code>ResultMessage</code> (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>).</li><li><strong>Rewind later by resuming with an empty prompt.</strong> Open a new client with <code>enable_file_checkpointing=True</code> and <code>resume=session_id</code>, send <code>""</code>, and call <code>rewind_files(checkpoint_id)</code> inside the response loop (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>).</li><li><strong>Or rewind from the shell, without SDK code:</strong></li></ol>
<pre data-lang="bash"><code class="language-bash">CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true \
  claude -p --resume &lt;session-id&gt; --rewind-files &lt;checkpoint-uuid&gt;</code></pre>
<p><code>--rewind-files</code> doesn't appear in <code>claude --help</code>, but the CLI accepts it. When it works, it prints <code>Files rewound to state at message &lt;checkpoint-uuid&gt;</code> and exits without sending a prompt. Without the environment variable, the bare CLI fails with "File rewinding is not enabled" (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>).</p>
<p>Run the rewind on the same machine that ran the job. Checkpoints belong to the session that created them (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>), and the snapshots live in local Claude Code state (<a href="https://code.claude.com/docs/en/checkpointing" rel="noopener">Checkpointing docs</a>). <code>--resume &lt;id&gt;</code> can find a session in other projects on the same machine (<a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference</a>). Running the rewind from the original project directory is still the safest choice.</p>
<p>Let the run finish before you depend on its checkpoints. "No file checkpoint found" can mean the session wasn't properly completed (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>), and a <code>claude -p</code> run stopped with SIGTERM leaves its current turn unfinished (<a href="https://code.claude.com/docs/en/headless" rel="noopener">Headless docs</a>). If your supervisor has to stop a job, send SIGINT or call the SDK's <code>interrupt()</code> first, as the headless docs recommend.</p>
<h3 id="using-only-the-cli-without-the-sdk">Using only the CLI, without the SDK</h3>
<p>You can do this from a shell script. <code>--replay-user-messages</code> requires both <code>--input-format stream-json</code> and <code>--output-format stream-json</code>. You can also give the session a known ID up front with <code>--session-id &lt;uuid&gt;</code> so you don't have to parse it from the output (<a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference</a>).</p>
<p>The SDK sets <code>CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING</code> internally only when checkpointing is enabled, and the bare CLI never sets it (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>). "No file checkpoint found" can mean checkpointing was off on the original session, so the safe reading is to set the variable on the original run as well as on the rewind. The docs don't describe this CLI-only flow end to end, so this is an inference. Test it on a throwaway repo before you rely on it.</p>
<p>Here is an example launch. The headless page doesn't show the stream-json input message format, so check it against the <a href="https://code.claude.com/docs/en/agent-sdk/overview" rel="noopener">Agent SDK docs</a> before use:</p>
<pre data-lang="bash"><code class="language-bash">SID=$(uuidgen | tr &#x27;A-Z&#x27; &#x27;a-z&#x27;)
echo &#x27;{&quot;type&quot;:&quot;user&quot;,&quot;message&quot;:{&quot;role&quot;:&quot;user&quot;,&quot;content&quot;:&quot;Fix lint errors in src/&quot;}}&#x27; | \
CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true claude -p \
  --session-id &quot;$SID&quot; \
  --input-format stream-json --output-format stream-json --verbose \
  --replay-user-messages --permission-mode acceptEdits \
  &gt; run.jsonl
CHECKPOINT=$(jq -r &#x27;select(.type==&quot;user&quot; and .uuid) | .uuid&#x27; run.jsonl | head -1)
echo &quot;$SID $CHECKPOINT&quot; &gt;&gt; agent-runs.log</code></pre>
<p>Don't use <code>--no-session-persistence</code> on runs you might want to rewind. It stops the session from being saved to disk, so the session can't be resumed (<a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference</a>), and a later rewind depends on resuming.</p>
<h2 id="rewind-vs-git-vs-worktrees-which-to-use-when">Rewind vs. git vs. worktrees: which to use when</h2>
<p>Use all of them together. Each one covers gaps the others leave.</p>
<div class="table-wrap"><table><thead><tr><th>Mechanism</th><th>Covers</th><th>Misses</th><th>Best for</th></tr></thead><tbody><tr><td>Checkpoint rewind</td><td>Write/Edit/NotebookEdit changes to local files (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>)</td><td>Bash changes, most subagent edits, directories, links, network files</td><td>Fast, precise undo of the agent's own edits, including going back to a middle turn</td></tr><tr><td>Git commit/branch before the run</td><td>Everything in the repo that git tracks</td><td>Untracked/ignored files unless you include them; anything outside the repo</td><td>The baseline you can always reset to</td></tr><tr><td><code>--worktree</code> / <code>-w</code></td><td>Runs in an isolated worktree at <code>&lt;repo&gt;/.claude/worktrees/&lt;name&gt;</code> (<a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference</a>)</td><td>Side effects outside the repo (APIs, databases)</td><td>Recurring jobs that should never touch your main checkout</td></tr><tr><td>Permission limits</td><td>Blocks risky actions up front, e.g. <code>--disallowedTools "Bash(rm *)"</code>, <code>--permission-prompts none</code> (v2.1.259+) (<a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference</a>; <a href="https://code.claude.com/docs/en/headless" rel="noopener">Headless docs</a>)</td><td>Mistakes made with allowed tools</td><td>Keeping the damage within what rewind and git can undo</td></tr></tbody></table></div>
<p>How to decide:</p>
<ul><li><strong>If you need to undo a run days later,</strong> check that the snapshots are still there (about 30 days by default) and raise <code>cleanupPeriodDays</code> if needed (<a href="https://code.claude.com/docs/en/checkpointing" rel="noopener">Checkpointing docs</a>).</li><li><strong>If the job runs where other people's settings could load</strong> (CI, shared machines), consider <code>--bare</code>. The docs recommend it for scripted calls. In bare mode Claude Code skips hooks, plugins, MCP servers and CLAUDE.md from the host, and it needs <code>ANTHROPIC_API_KEY</code> instead of a subscription login (<a href="https://code.claude.com/docs/en/headless" rel="noopener">Headless docs</a>).</li></ul>
<h2 id="a-practical-setup-checklist">A practical setup checklist</h2>
<ol><li>Start each run on a clean git state, or in a worktree with <code>claude -w &lt;name&gt;</code> (<a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference</a>).</li><li>Turn on checkpointing (<code>enableFileCheckpointing</code> / <code>enable_file_checkpointing</code>, or the env var for the bare CLI) and <code>replay-user-messages</code> (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>).</li><li>Set the permission mode explicitly (<a href="https://code.claude.com/docs/en/headless" rel="noopener">Headless docs</a>).</li><li>Save the first user-message UUID and the session ID for every run.</li><li>Limit runaway runs with <code>--max-turns</code> and <code>--max-budget-usd</code>, which work in print mode only. The budget cap uses a client-side cost estimate that can differ from your bill, and spend can go past it, so leave headroom (<a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference</a>).</li><li>Add a validation step (tests, lint, diff-size check) and rewind automatically when it fails.</li><li>After any rewind, run <code>git status</code>. Anything still changed came from Bash, a subagent or a skipped path. Reset it with git.</li><li>Read <code>skippedLinks</code> in <code>RewindFilesResult</code> and alert when it isn't zero, because those files kept their current contents (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>).</li></ol>
<h2 id="troubleshooting-quick-reference">Troubleshooting quick reference</h2>
<p>Most rewind failures come from a missing option on the original run or from calling rewind at the wrong time.</p>
<div class="table-wrap"><table><thead><tr><th>Symptom</th><th>Likely cause</th><th>Fix</th></tr></thead><tbody><tr><td><code>message.uuid</code> missing</td><td><code>replay-user-messages</code> not set</td><td>Add it to <code>extraArgs</code> / <code>extra_args</code> (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>)</td></tr><tr><td>"No file checkpoint found for this message"</td><td>Checkpointing was off on the original session, or the session didn't finish</td><td>Enable it on the original run and let the session finish before resuming (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">same</a>)</td></tr><tr><td>"File rewinding is not enabled"</td><td>Bare CLI without the env var, or a resumed SDK session without the option</td><td>Set <code>CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true</code>, or enable the option on the resumed session (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">same</a>)</td></tr><tr><td>"ProcessTransport is not ready for writing"</td><td>Rewind called after the loop ended</td><td>Resume with an empty prompt, then rewind (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">same</a>)</td></tr><tr><td>"No files were restored"</td><td>Snapshots removed by retention cleanup</td><td>Raise <code>cleanupPeriodDays</code>; use git for this run (<a href="https://code.claude.com/docs/en/checkpointing" rel="noopener">Checkpointing docs</a>)</td></tr><tr><td>Files changed after a "successful" rewind</td><td>Bash edits (including <code>mv</code>/<code>cp</code> allowed by <code>acceptEdits</code>), subagent edits, or skipped paths</td><td><code>git status</code>, then <code>git checkout</code>/<code>git clean</code> the leftovers</td></tr><tr><td>Options not recognized</td><td>Old SDK</td><td><code>pip install --upgrade claude-agent-sdk</code> or <code>npm install @anthropic-ai/claude-agent-sdk@latest</code> (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>)</td></tr></tbody></table></div>
<h2 id="faq">FAQ</h2>
<h3 id="are-checkpoints-on-by-default-in-claude-p-or-the-agent-sdk">Are checkpoints on by default in <code>claude -p</code> or the Agent SDK?</h3>
<p>No. Interactive sessions take snapshots by default. In the Agent SDK you have to enable file checkpointing and <code>replay-user-messages</code>, and with the bare CLI you set <code>CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true</code> (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>).</p>
<h3 id="can-i-skip-git-if-i-use-checkpoints">Can I skip git if I use checkpoints?</h3>
<p>No. Anthropic says checkpoints are for quick, session-level recovery and recommends git for permanent history. Rewind also misses Bash changes, most subagent edits and directory operations (<a href="https://code.claude.com/docs/en/checkpointing" rel="noopener">Checkpointing docs</a>).</p>
<h3 id="does-rewinding-files-also-rewind-the-conversation">Does rewinding files also rewind the conversation?</h3>
<p>Not in the SDK. <code>rewindFiles()</code> / <code>rewind_files()</code> restores files on disk and leaves the conversation history as it was. The interactive <code>/rewind</code> menu lets you choose code, conversation, or both (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>; <a href="https://code.claude.com/docs/en/checkpointing" rel="noopener">Checkpointing docs</a>).</p>
<h3 id="how-long-can-i-wait-before-rewinding-a-run">How long can I wait before rewinding a run?</h3>
<p>By default, about 30 days after the session last saved a snapshot. After that, cleanup can remove the snapshots. You can change this with <code>cleanupPeriodDays</code> (<a href="https://code.claude.com/docs/en/checkpointing" rel="noopener">Checkpointing docs</a>).</p>
<h3 id="can-i-rewind-on-a-different-machine-such-as-a-fresh-ci-runner">Can I rewind on a different machine, such as a fresh CI runner?</h3>
<p>Not reliably. Checkpoints are tied to the session that created them, and the snapshots live in local Claude Code state (<a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">SDK docs</a>; <a href="https://code.claude.com/docs/en/checkpointing" rel="noopener">Checkpointing docs</a>). On short-lived runners, rely on git or worktrees instead.</p>
<h2 id="next-steps">Next steps</h2>
<p>Pick the job type that matches your automation and try only the checklist items for that type today. Each list takes one throwaway repo and one run.</p>
<p><strong>If your job mostly edits files (Edit/Write):</strong></p>
<ol><li>Copy the Pattern 1 code. Replace <code>should_revert()</code> with one check, such as "tests fail."</li><li>Run it once on a throwaway repo. Make the check fail on purpose and confirm the files go back.</li><li>Run <code>git status</code> afterwards. If something is still changed, find which command caused it.</li></ol>
<p><strong>If your job mostly runs Bash commands:</strong></p>
<ol><li>Wrap the run in <code>claude -w &lt;name&gt;</code>, or commit before each run.</li><li>Write a one-line revert script: <code>git reset --hard &amp;&amp; git clean -fd</code>, run inside the worktree only.</li><li>Add <code>--disallowedTools "Bash(rm *)"</code> and <code>--max-turns</code>, then do one test run.</li></ol>
<p><strong>If your job uses subagents or forked skills:</strong></p>
<ol><li>Assume rewind won't restore their edits, and run the job in a worktree.</li><li>Save the session ID and first UUID anyway (Pattern 2), so the main agent's own edits can still be rewound.</li><li>After a test run, compare <code>git diff</code> with the rewind result to see what the subagents changed.</li></ol>
<p><strong>If you'd rather not build this yourself:</strong> a freelance developer who knows the Agent SDK and your CI can usually set up a worktree, validation and revert script for you. You pay for their time on top of your usual Claude usage. Ask for a fixed-scope quote, because rates vary too much to give a reliable figure here. This suits you if you have one important recurring job and little time. It's a poor fit if your jobs change every week, because you'll keep paying for changes. Another route is to run the job in GitHub Actions or GitLab CI with Claude Code (<a href="https://code.claude.com/docs/en/github-actions" rel="noopener">GitHub Actions docs</a>, <a href="https://code.claude.com/docs/en/gitlab-ci-cd" rel="noopener">GitLab CI/CD docs</a>). Every run then starts from a clean checkout, and you undo it by not merging its branch, so you don't depend on checkpoints at all.</p>
<p><strong>Related on Unattended:</strong> look for our guides on running <code>claude -p</code> on a schedule, choosing permission modes for unattended runs, and using git worktrees for agent jobs. They cover the layers that this article treats as "use git instead."</p>
<h2 id="sources">Sources</h2>
<p>Checked on 2026-10-15.</p>
<ul><li><a href="https://code.claude.com/docs/en/checkpointing" rel="noopener">Checkpointing – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/agent-sdk/file-checkpointing" rel="noopener">Rewind file changes with checkpointing – Agent SDK Docs</a></li><li><a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/settings-reference" rel="noopener">Settings reference – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/headless" rel="noopener">Run Claude Code programmatically (headless) – Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/agent-sdk/overview" rel="noopener">Agent SDK overview – Claude Code Docs</a></li></ul><p>Originally published at <a href="https://en.aobaworks.com/claude-code-checkpoints-how-rollback-works-in-automations/">Unattended</a> (updated 2026-10-15).</p>]]></content:encoded><enclosure url="https://en.aobaworks.com/og/claude-code-checkpoints-how-rollback-works-in-automations.png" type="image/png" length="48464"/><media:content url="https://en.aobaworks.com/og/claude-code-checkpoints-how-rollback-works-in-automations.png" medium="image" type="image/png" width="1200" height="630"/></item><item><title>Why Claude Code Stops Mid-Automation (and How to Fix It)</title><link>https://en.aobaworks.com/why-claude-code-stops-mid-automation-and-how-to-fix-it/</link><guid isPermaLink="true">https://en.aobaworks.com/why-claude-code-stops-mid-automation-and-how-to-fix-it/</guid><pubDate>Tue, 13 Oct 2026 03:00:00 GMT</pubDate><dc:creator>Aoba Works</dc:creator><description>Why does Claude Code stop running headless automations? Map the turn, budget, timeout and permission triggers in claude -p runs, plus a fix checklist.</description><category>claude code</category><category>headless</category><category>automation</category><category>ci</category><category>agent sdk</category><category>permissions</category><content:encoded><![CDATA[<figure><img src="https://en.aobaworks.com/og/why-claude-code-stops-mid-automation-and-how-to-fix-it.png" alt="Why Claude Code Stops Mid-Automation (and How to Fix It)" width="1200" height="630"></figure><p class="meta ai-note">How this guide was made: drafted with AI assistance from the official sources linked below, then passed an automated editorial check before publishing. <a href="https://en.aobaworks.com/about/#how-guides-are-made">Our process</a></p><p>A headless Claude Code run (<code>claude -p</code>) usually stops for one of three reasons. It reached a limit you set, such as <code>--max-turns</code> or <code>--max-budget-usd</code>. 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 <code>subtype</code> and exit code after every run, and give the agent a clear, pre-approved set of tools.</p>
<p>This guide goes through each trigger using Anthropic's own documentation, then ends with a checklist you can copy into your scheduled jobs.</p>
<h2 id="how-to-see-why-a-run-stopped">How to see why a run stopped</h2>
<p>Before changing any settings, make every run report why it ended. Claude Code <a href="https://code.claude.com/docs/en/headless" rel="noopener">exits with code 0 on success and a non-zero code when the run fails</a>. One catch: when the failure happens <em>inside</em> the run (missing authentication, for example), <a href="https://code.claude.com/docs/en/headless" rel="noopener">the failure is printed as the result on stdout</a>, not on stderr. If your cron script only logs stderr, you will miss it.</p>
<p>Use JSON output so you can read the result in your script:</p>
<pre data-lang="bash"><code class="language-bash">claude -p &quot;Triage new issues&quot; --output-format json &gt; run.json
jq -r &#x27;.subtype, .stop_reason, .total_cost_usd, .num_turns&#x27; run.json</code></pre>
<p>The Agent SDK docs list these <a href="https://code.claude.com/docs/en/agent-sdk/agent-loop" rel="noopener">result subtypes</a>: <code>success</code>, <code>error_max_turns</code>, <code>error_max_budget_usd</code>, <code>error_during_execution</code> and <code>error_max_structured_output_retries</code>. Every result includes <code>total_cost_usd</code>, <code>usage</code>, <code>num_turns</code> and <code>session_id</code>. The <code>stop_reason</code> field can also be <code>max_tokens</code>, which means the model hit its output token limit on the last turn.</p>
<p>For more detail, use <code>--output-format stream-json --verbose</code>. In that format, <a href="https://code.claude.com/docs/en/headless" rel="noopener">permission denials show up as <code>permission_denied</code> messages, and the final result lists them in <code>permission_denials</code></a>. Retryable API errors show up as <code>system/api_retry</code> events with a category such as <code>rate_limit</code>, <code>overloaded</code>, <code>authentication_failed</code> or <code>max_output_tokens</code> (<a href="https://code.claude.com/docs/en/headless" rel="noopener">source</a>).</p>
<h2 id="trigger-1-turn-and-token-budgets">Trigger 1: Turn and token budgets</h2>
<h3 id="max-turns">Max turns</h3>
<p><code>--max-turns</code> limits the number of agentic turns in print mode. It has <a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">no limit by default and exits with an error when the limit is reached</a>. 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 <code>--max-turns 3</code> to keep a nightly job small, it can stop before the edit step. The SDK docs give this same example, where <a href="https://code.claude.com/docs/en/agent-sdk/agent-loop" rel="noopener"><code>max_turns=2</code> stops before the edit</a>.</p>
<p><strong>Fix:</strong> Give the job some room above the number of turns it normally needs, and treat <code>error_max_turns</code> as "resume later", not as a failure. The session ID still comes back, so you can run <code>claude -p "continue" --resume "$session_id"</code> (<a href="https://code.claude.com/docs/en/headless" rel="noopener">source</a>).</p>
<h3 id="dollar-budget">Dollar budget</h3>
<p><code>--max-budget-usd</code> stops the run <a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">once estimated spend reaches the amount you set</a>. Things to know:</p>
<ul><li>The check uses a client-side cost estimate, which <a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">can differ from your bill</a>.</li><li>Spend can go past the cap. The response that reaches it still finishes, and running subagents keep spending until they stop. Anthropic says to <a href="https://code.claude.com/docs/en/agent-sdk/agent-loop" rel="noopener">leave headroom</a>.</li><li>Subagent spend counts toward the cap. After the cap is reached, starting another subagent fails with <code>Budget limit reached</code>, and background subagents are stopped (<a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">source</a>).</li><li>When you continue a run with <code>--continue</code> or <code>--resume</code>, the totals from earlier runs <a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">don't count toward the new cap</a>.</li></ul>
<h3 id="output-tokens-and-context">Output tokens and context</h3>
<p><code>CLAUDE_CODE_MAX_OUTPUT_TOKENS</code> sets the maximum output for most requests. Defaults and caps depend on the model, and <a href="https://code.claude.com/docs/en/env-vars" rel="noopener">raising it reduces the effective context window available before auto-compaction</a>. If a long file write ends with <code>stop_reason: max_tokens</code>, this setting is the place to look.</p>
<p>Long runs also fill up the context window. When it gets close to the limit, Claude Code <a href="https://code.claude.com/docs/en/agent-sdk/agent-loop" rel="noopener">summarizes older history, so instructions from early in the conversation may not be kept</a>. 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 <code>--bare</code>, in <code>--append-system-prompt-file</code>. Keep tool output small: Bash output read back into context is capped by <code>BASH_MAX_OUTPUT_LENGTH</code> (default 30,000 characters) (<a href="https://code.claude.com/docs/en/env-vars" rel="noopener">source</a>).</p>
<h2 id="trigger-2-session-and-tool-timeouts">Trigger 2: Session and tool timeouts</h2>
<p>Several timers apply to a headless run:</p>
<div class="table-wrap"><table><thead><tr><th>Timer</th><th>Default</th><th>What happens</th><th>Setting</th></tr></thead><tbody><tr><td>Foreground Bash command</td><td>2 min</td><td>Command times out</td><td><a href="https://code.claude.com/docs/en/env-vars" rel="noopener"><code>BASH_DEFAULT_TIMEOUT_MS</code></a></td></tr><tr><td>Max Bash timeout Claude can request</td><td>10 min</td><td>Ceiling for long commands</td><td><a href="https://code.claude.com/docs/en/env-vars" rel="noopener"><code>BASH_MAX_TIMEOUT_MS</code></a></td></tr><tr><td>Idle wait for background work after the final turn</td><td>10 min</td><td>Running work is stopped and its partial result dropped</td><td><a href="https://code.claude.com/docs/en/headless" rel="noopener"><code>CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS</code></a> (<code>0</code> = no cap)</td></tr><tr><td>Monitor watch</td><td>5 min</td><td>Watch times out</td><td><a href="https://code.claude.com/docs/en/headless" rel="noopener">Headless docs</a></td></tr><tr><td>MCP server startup with <code>--mcp-config</code></td><td>30 s</td><td>Run starts without the pending server</td><td><a href="https://code.claude.com/docs/en/cli-reference" rel="noopener"><code>MCP_TIMEOUT</code></a></td></tr><tr><td>API request</td><td>10 min</td><td>Request fails</td><td><a href="https://code.claude.com/docs/en/env-vars" rel="noopener"><code>API_TIMEOUT_MS</code></a></td></tr></tbody></table></div>
<p>Some details matter here:</p>
<ul><li><strong>Bare mode:</strong> with <code>--bare</code>, no background tasks run, so <a href="https://code.claude.com/docs/en/headless" rel="noopener">a command that reaches its timeout stops instead of moving to the background</a>. This affects long test suites or builds.</li><li><strong>Missing MCP servers don't fail the run.</strong> Claude Code skips invalid <code>--mcp-config</code> entries and <a href="https://code.claude.com/docs/en/headless" rel="noopener">the run continues and exits cleanly</a>. When stderr is captured (as in CI), it prints no warning. The skipped entries only appear in <code>mcp_server_errors</code> in the <code>system/init</code> event. A CI gate can check whether that array is empty.</li><li><strong>Your own scheduler can stop the run.</strong> If a CI runner or process supervisor sends SIGTERM, Claude Code <a href="https://code.claude.com/docs/en/headless" rel="noopener">exits with code 143 and records no result for the unfinished turn</a>. To end the turn cleanly, send SIGINT first. To continue the interrupted turn when you resume, set <code>CLAUDE_CODE_RESUME_INTERRUPTED_TURN=1</code> (<a href="https://code.claude.com/docs/en/headless" rel="noopener">source</a>).</li><li><strong>Large piped input fails.</strong> Piped stdin is <a href="https://code.claude.com/docs/en/headless" rel="noopener">capped at 10MB</a>. For bigger inputs, write them to a file and put the file path in the prompt.</li></ul>
<h3 id="credentials-expire-too">Credentials expire too</h3>
<p>Login expiry is not a timeout setting, but on a schedule it has the same effect. Once a <code>/login</code> session expires, <a href="https://code.claude.com/docs/en/authentication" rel="noopener">each model request fails with <code>Login expired</code></a>. For scripts, <code>claude setup-token</code> creates a <a href="https://code.claude.com/docs/en/authentication" rel="noopener">one-year OAuth token</a> that you set as <code>CLAUDE_CODE_OAUTH_TOKEN</code>. However, <a href="https://code.claude.com/docs/en/authentication" rel="noopener">bare mode does not read <code>CLAUDE_CODE_OAUTH_TOKEN</code></a>. With <code>--bare</code>, use <code>ANTHROPIC_API_KEY</code> or an <code>apiKeyHelper</code> instead. Also note that in <code>-p</code> mode, a set <code>ANTHROPIC_API_KEY</code> is <a href="https://code.claude.com/docs/en/authentication" rel="noopener">always used when present</a>. An old key left in your shell environment will therefore override your subscription login.</p>
<h2 id="trigger-3-permission-prompts-with-no-one-to-answer">Trigger 3: Permission prompts with no one to answer</h2>
<p>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":</p>
<ul><li><strong>No host (plain <code>claude -p</code>):</strong> any request that would show a prompt <a href="https://code.claude.com/docs/en/headless" rel="noopener">is denied</a>. <a href="https://code.claude.com/docs/en/agent-sdk/agent-loop" rel="noopener">Claude gets a rejection message and usually tries something else or reports that it couldn't continue</a>. From outside, the job looks like it quit early.</li><li><strong>With a host</strong> (an SDK <code>canUseTool</code> callback or <code>--permission-prompt-tool</code>): <a href="https://code.claude.com/docs/en/headless" rel="noopener">the run waits for that host to answer each request</a>. If the host is not answering, the run can wait until a timer ends it.</li></ul>
<p>For unattended jobs, pass <code>--permission-prompts none</code> (v2.1.259 or later). <a href="https://code.claude.com/docs/en/headless" rel="noopener">Anything that would prompt is denied unless a <code>PermissionRequest</code> hook allows it, and Claude is told not to retry</a>.</p>
<h3 id="set-the-permission-mode-explicitly">Set the permission mode explicitly</h3>
<p>If nothing sets a permission mode, the run uses the built-in starting mode, <a href="https://code.claude.com/docs/en/headless" rel="noopener">which can be <code>auto</code></a>. Choose the mode yourself:</p>
<div class="table-wrap"><table><thead><tr><th>Mode</th><th>Runs without asking</th><th>Good fit for headless</th></tr></thead><tbody><tr><td><code>dontAsk</code></td><td>Pre-approved tools, reads in working dirs, read-only commands; <a href="https://code.claude.com/docs/en/agent-sdk/agent-loop" rel="noopener">everything else denied</a></td><td>Locked-down recurring jobs</td></tr><tr><td><code>acceptEdits</code></td><td>File edits plus <code>mkdir</code>, <code>touch</code>, <code>mv</code>, <code>cp</code>; <a href="https://code.claude.com/docs/en/headless" rel="noopener">other shell commands still need allow rules</a></td><td>Lint or formatting fixers</td></tr><tr><td><code>auto</code></td><td>A classifier reviews each action (<a href="https://code.claude.com/docs/en/headless" rel="noopener">source</a>)</td><td>Broader tasks where you still want guardrails</td></tr><tr><td><code>bypassPermissions</code></td><td>All tools except explicit <code>ask</code> rules and similar exceptions. <a href="https://code.claude.com/docs/en/agent-sdk/agent-loop" rel="noopener">Can't run as root on Unix; only for isolated environments</a></td><td>Throwaway containers only</td></tr></tbody></table></div>
<h3 id="allow-rule-mistakes-that-lead-to-denials">Allow-rule mistakes that lead to denials</h3>
<ul><li>**Missing space before <code>*</code>.** <code>Bash(git diff <em>)</code> matches commands starting with <code>git diff</code>. <a href="https://code.claude.com/docs/en/headless" rel="noopener">Without the space, <code>Bash(git diff</em>)</code> would also match <code>git diff-index</code></a>.</li><li><strong>Bare <code>Bash</code> in auto mode.</strong> In a run that starts in auto mode, <a href="https://code.claude.com/docs/en/headless" rel="noopener">a bare <code>Bash</code> entry is dropped</a> and the classifier evaluates each command instead.</li><li><strong>Tools that <code>dontAsk</code> always denies.</strong> <code>AskUserQuestion</code>, MCP tools marked <code>requiresUserInteraction</code> and reads from network paths <a href="https://code.claude.com/docs/en/headless" rel="noopener">are denied even when an allow rule matches</a>. If your prompt tells Claude to "ask me if unsure", that question will be denied in a headless run.</li></ul>
<p>One more thing: without <code>--bare</code>, a <code>-p</code> run <a href="https://code.claude.com/docs/en/headless" rel="noopener">executes the hooks in the project's <code>.claude/settings.json</code> and connects its <code>.mcp.json</code> servers, even in an untrusted folder</a>. That can change how a job behaves from one machine to another. Anthropic recommends <code>--bare</code> for scripted calls and says it <a href="https://code.claude.com/docs/en/headless" rel="noopener">will become the default for <code>-p</code></a>.</p>
<h2 id="a-prevention-checklist">A prevention checklist</h2>
<p>Here is an example nightly job that applies the points above. Adjust the paths, tools and numbers for your own job.</p>
<pre data-lang="bash"><code class="language-bash">#!/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 &quot;Update dependency pins and run the tests&quot; \
  --append-system-prompt-file ./agent-rules.md \
  --permission-mode dontAsk \
  --permission-prompts none \
  --allowedTools &quot;Read,Edit,Bash(npm test *),Bash(npm install *),Bash(git diff *)&quot; \
  --max-turns 40 \
  --max-budget-usd 3.00 \
  --output-format json &gt; run.json
code=$?

subtype=$(jq -r &#x27;.subtype&#x27; run.json)
[ &quot;$code&quot; -ne 0 ] || [ &quot;$subtype&quot; != &quot;success&quot; ] &amp;&amp; echo &quot;Agent stopped: $subtype (exit $code)&quot; &gt;&amp;2</code></pre>
<p>Go through this list before you schedule a job:</p>
<p><strong>Budgets</strong></p>
<ul><li>[ ] <code>--max-turns</code> is set with headroom, and <code>error_max_turns</code> triggers a resume or an alert.</li><li>[ ] <code>--max-budget-usd</code> is set a bit below your real limit, since spend can go past it.</li><li>[ ] Permanent rules live in CLAUDE.md or <code>--append-system-prompt-file</code>, not only in the opening prompt, so compaction doesn't drop them.</li><li>[ ] Noisy commands are piped through <code>tail</code> or <code>grep</code> so they don't fill the context window.</li></ul>
<p><strong>Timeouts</strong></p>
<ul><li>[ ] <code>BASH_DEFAULT_TIMEOUT_MS</code> / <code>BASH_MAX_TIMEOUT_MS</code> are longer than your slowest build or test command.</li><li>[ ] <code>CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS</code> fits jobs that use background subagents.</li><li>[ ] The CI or job timeout is longer than the agent's own limits, and it sends SIGINT before SIGTERM.</li><li>[ ] Your gate fails when <code>mcp_server_errors</code> is not empty.</li><li>[ ] Authentication matches the mode: API key or <code>apiKeyHelper</code> with <code>--bare</code>, <code>CLAUDE_CODE_OAUTH_TOKEN</code> without it. You have a reminder before the one-year token expires.</li></ul>
<p><strong>Permissions</strong></p>
<ul><li>[ ] The permission mode is set explicitly (<code>dontAsk</code> is a sensible default).</li><li>[ ] <code>--permission-prompts none</code> is set so nothing waits on a host.</li><li>[ ] Every command the task needs is in <code>--allowedTools</code>, with correct <code>*</code> spacing.</li><li>[ ] The prompt never asks Claude to check with a human.</li><li>[ ] The script logs <code>permission_denials</code> after each run, and you add allow rules (or narrow the task) based on what it shows.</li></ul>
<h2 id="when-to-raise-a-limit-and-when-to-narrow-the-task">When to raise a limit and when to narrow the task</h2>
<p>If a job keeps hitting <code>error_max_turns</code> or <code>error_max_budget_usd</code>, 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 <a href="https://code.claude.com/docs/en/agent-sdk/agent-loop" rel="noopener">start with a fresh context and only return a summary</a>), 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.</p>
<h2 id="sources">Sources</h2>
<ul><li><a href="https://code.claude.com/docs/en/headless" rel="noopener">Run Claude Code programmatically (headless mode)</a></li><li><a href="https://code.claude.com/docs/en/cli-reference" rel="noopener">CLI reference</a></li><li><a href="https://code.claude.com/docs/en/agent-sdk/agent-loop" rel="noopener">How the agent loop works (Agent SDK)</a></li><li><a href="https://code.claude.com/docs/en/env-vars" rel="noopener">Environment variables</a></li><li><a href="https://code.claude.com/docs/en/authentication" rel="noopener">Authentication</a></li><li><a href="https://code.claude.com/docs/en/permission-modes" rel="noopener">Choose a permission mode</a></li></ul><p>Originally published at <a href="https://en.aobaworks.com/why-claude-code-stops-mid-automation-and-how-to-fix-it/">Unattended</a>.</p>]]></content:encoded><enclosure url="https://en.aobaworks.com/og/why-claude-code-stops-mid-automation-and-how-to-fix-it.png" type="image/png" length="51361"/><media:content url="https://en.aobaworks.com/og/why-claude-code-stops-mid-automation-and-how-to-fix-it.png" medium="image" type="image/png" width="1200" height="630"/></item></channel></rss>
