<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aoba Works</title>
    <description>The latest articles on DEV Community by Aoba Works (@aobaworks).</description>
    <link>https://dev.to/aobaworks</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4190387%2F73db9167-55f9-43bd-b136-eb2f99919c55.png</url>
      <title>DEV Community: Aoba Works</title>
      <link>https://dev.to/aobaworks</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aobaworks"/>
    <language>en</language>
    <item>
      <title>Claude Code Hooks Lifecycle: Every Event, Explained</title>
      <dc:creator>Aoba Works</dc:creator>
      <pubDate>Sun, 18 Oct 2026 16:44:55 +0000</pubDate>
      <link>https://dev.to/aobaworks/claude-code-hooks-lifecycle-every-event-explained-5g6</link>
      <guid>https://dev.to/aobaworks/claude-code-hooks-lifecycle-every-event-explained-5g6</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://en.aobaworks.com/claude-code-hooks-lifecycle-every-event-explained/?utm_source=devto&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unattended&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;SessionStart&lt;/code&gt;, loops through &lt;code&gt;UserPromptSubmit&lt;/code&gt; → &lt;code&gt;PreToolUse&lt;/code&gt; → &lt;code&gt;PostToolUse&lt;/code&gt;/&lt;code&gt;PostToolUseFailure&lt;/code&gt; → &lt;code&gt;PostToolBatch&lt;/code&gt; for each step, ends each turn with &lt;code&gt;Stop&lt;/code&gt; (or &lt;code&gt;StopFailure&lt;/code&gt; when the API errors), and finishes with &lt;code&gt;SessionEnd&lt;/code&gt; (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;). Four of these events do most of the work in unattended automations. &lt;code&gt;PreToolUse&lt;/code&gt;, &lt;code&gt;PostToolBatch&lt;/code&gt; and &lt;code&gt;Stop&lt;/code&gt; can halt runaway work. &lt;code&gt;StopFailure&lt;/code&gt; reports runs that died quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which option is right for you?
&lt;/h2&gt;

&lt;p&gt;Choose your first hook based on the problem you have now. The table below matches common situations to an event.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;What to pick first&lt;/th&gt;
&lt;th&gt;First page to open&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Beginner, never written a hook&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;Notification&lt;/code&gt; hook that alerts you when Claude is waiting&lt;/td&gt;
&lt;td&gt;&lt;a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener noreferrer"&gt;Hooks guide: Set up your first hook&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily automation that sometimes loops or burns usage&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;PreToolUse&lt;/code&gt; or &lt;code&gt;PostToolBatch&lt;/code&gt; budget guard&lt;/td&gt;
&lt;td&gt;&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference: Decision control&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overnight or cron runs that "finish" with nothing done&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;StopFailure&lt;/code&gt; + &lt;code&gt;Stop&lt;/code&gt; hooks that log and verify&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt; and /claude-code-cron-job-setup-run-automations-overnight/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tight budget (Pro plan, shared usage limits)&lt;/td&gt;
&lt;td&gt;Command hooks only. Skip &lt;code&gt;prompt&lt;/code&gt;/&lt;code&gt;agent&lt;/code&gt; hooks&lt;/td&gt;
&lt;td&gt;&lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;Claude pricing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why each pick makes sense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Beginner:&lt;/strong&gt; a notification hook can't block anything, so a mistake in it won't break your session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily use:&lt;/strong&gt; you can block at &lt;code&gt;PreToolUse&lt;/code&gt; and &lt;code&gt;PostToolBatch&lt;/code&gt; before the next model call, so you stop the spending instead of just recording it (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unattended runs:&lt;/strong&gt; &lt;code&gt;StopFailure&lt;/code&gt; matches error types such as &lt;code&gt;rate_limit&lt;/code&gt; and &lt;code&gt;billing_error&lt;/code&gt;, which otherwise show up as a quiet exit (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget:&lt;/strong&gt; &lt;code&gt;prompt&lt;/code&gt; hooks send text to a model and &lt;code&gt;agent&lt;/code&gt; hooks run a subagent, so both use model calls. Command hooks are just your own scripts and cost nothing extra.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The lifecycle of one automation run, in order
&lt;/h2&gt;

&lt;p&gt;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 (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Fires when&lt;/th&gt;
&lt;th&gt;Can block?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Start&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SessionStart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Session begins or resumes (matcher: &lt;code&gt;startup&lt;/code&gt;, &lt;code&gt;resume&lt;/code&gt;, &lt;code&gt;clear&lt;/code&gt;, &lt;code&gt;compact&lt;/code&gt;, &lt;code&gt;fork&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Start&lt;/td&gt;
&lt;td&gt;&lt;code&gt;InstructionsLoaded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A CLAUDE.md or &lt;code&gt;.claude/rules/*.md&lt;/code&gt; file is loaded&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UserPromptSubmit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A prompt is submitted, before Claude processes it, including turns Claude Code starts by itself&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool loop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PreToolUse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Before a tool call executes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool loop&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PermissionRequest&lt;/code&gt; / &lt;code&gt;PermissionDenied&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A call needs a permission decision / auto mode denied it&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool loop&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PostToolUse&lt;/code&gt; / &lt;code&gt;PostToolUseFailure&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;After a tool call succeeds / fails&lt;/td&gt;
&lt;td&gt;No (exit code), but &lt;code&gt;decision: "block"&lt;/code&gt; feeds back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool loop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PostToolBatch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;After a batch of parallel tool calls resolves, &lt;strong&gt;before the next model call&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delegation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SubagentStart&lt;/code&gt; / &lt;code&gt;SubagentStop&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Subagent spawned / finished&lt;/td&gt;
&lt;td&gt;Stop: Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PreCompact&lt;/code&gt; / &lt;code&gt;PostCompact&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Before / after context compaction (&lt;code&gt;manual&lt;/code&gt; or &lt;code&gt;auto&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Pre: Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turn end&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude finishes responding&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turn end&lt;/td&gt;
&lt;td&gt;&lt;code&gt;StopFailure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Turn ends because of an API error&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Waiting&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Notification&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude Code sends a notification (e.g. &lt;code&gt;permission_prompt&lt;/code&gt;, &lt;code&gt;idle_prompt&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SessionEnd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Session terminates&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The reference also lists events for tasks, teams, worktrees, model switches, MCP elicitation and file changes, such as &lt;code&gt;TaskCompleted&lt;/code&gt;, &lt;code&gt;TeammateIdle&lt;/code&gt; and &lt;code&gt;FileChanged&lt;/code&gt;. Most solo setups don't need them on day one (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Three timing facts that matter
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All matching hooks run in parallel&lt;/strong&gt;, so you can't rely on one hook finishing before another starts (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default timeouts differ by event.&lt;/strong&gt; Most hooks get 600 seconds, &lt;code&gt;UserPromptSubmit&lt;/code&gt; gets 30, and all &lt;code&gt;SessionEnd&lt;/code&gt; hooks share a 1.5-second budget that can be raised to at most 60 seconds (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;). Keep cleanup work at session end small.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hooks from different places add up.&lt;/strong&gt; User, project, local, plugin, skill and subagent hooks all run; a lower level doesn't replace a higher one (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which hooks actually stop runaway token use
&lt;/h2&gt;

&lt;p&gt;Only hooks that fire &lt;em&gt;before&lt;/em&gt; the next model call can stop a runaway run. In practice that means &lt;code&gt;PreToolUse&lt;/code&gt;, &lt;code&gt;PostToolBatch&lt;/code&gt;, &lt;code&gt;UserPromptSubmit&lt;/code&gt; and the universal &lt;code&gt;continue: false&lt;/code&gt; field. Logging hooks like &lt;code&gt;PostToolUse&lt;/code&gt; only record what already happened.&lt;/p&gt;

&lt;h3&gt;
  
  
  The kill switch: &lt;code&gt;continue: false&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Any hook can return JSON with &lt;code&gt;"continue": false&lt;/code&gt;. The reference says this "stops Claude entirely," and &lt;code&gt;stopReason&lt;/code&gt; explains why (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;). It's the most reliable way for an unattended run to stop itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;PostToolBatch&lt;/code&gt;: the per-step budget checkpoint
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PostToolBatch&lt;/code&gt; fires after a whole batch of tool calls resolves and before Claude makes its next model call (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;). That makes it a good place to count steps. For example, a script could keep a counter file in the session's &lt;code&gt;scratchpad_dir&lt;/code&gt; (an input field available from v2.1.257; see the &lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt; and &lt;a href="https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;Claude Code changelog&lt;/a&gt;). Once the count passes a limit you choose, it returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"continue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"stopReason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Step budget of 80 tool batches reached. Stopping for review."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The number 80 is only an example. Choose a limit based on how many steps your normal runs take.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;PreToolUse&lt;/code&gt;: block the expensive call itself
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PreToolUse&lt;/code&gt; returns &lt;code&gt;permissionDecision&lt;/code&gt; with the value &lt;code&gt;allow&lt;/code&gt;, &lt;code&gt;deny&lt;/code&gt;, &lt;code&gt;ask&lt;/code&gt; or &lt;code&gt;defer&lt;/code&gt;. It can also rewrite the call with &lt;code&gt;updatedInput&lt;/code&gt; (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;). Use a matcher such as &lt;code&gt;Bash&lt;/code&gt; or &lt;code&gt;mcp__.*&lt;/code&gt; and the &lt;code&gt;if&lt;/code&gt; field (for example &lt;code&gt;"Bash(git *)"&lt;/code&gt;) so the hook only runs on the calls you care about. One limitation: &lt;code&gt;deny&lt;/code&gt; blocks that one call, and Claude may try a different approach afterwards. Pair it with a counter if you want the run to end.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Stop&lt;/code&gt;: useful, but it can &lt;em&gt;add&lt;/em&gt; usage
&lt;/h3&gt;

&lt;p&gt;When a &lt;code&gt;Stop&lt;/code&gt; hook exits with code 2, Claude doesn't stop and the conversation keeps going (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;). 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 &lt;code&gt;stop_hook_active&lt;/code&gt; input field, which is &lt;code&gt;true&lt;/code&gt; when Claude is already continuing because a Stop hook blocked it. If it's true, exit 0 (&lt;a href="https://amitkoth.com/claude-code-stop-hooks/" rel="noopener noreferrer"&gt;Amit Kothari&lt;/a&gt;, &lt;a href="https://claudefa.st/blog/tools/hooks/stop-hook-task-enforcement" rel="noopener noreferrer"&gt;claudefa.st&lt;/a&gt;). A &lt;a href="https://github.com/anthropics/claude-code/issues/54360" rel="noopener noreferrer"&gt;GitHub issue (#54360)&lt;/a&gt; reports cases where the flag didn't propagate, so add a hard cap as well. For example, allow at most 3 blocks per session.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;PreCompact&lt;/code&gt;: an early warning about long runs
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PreCompact&lt;/code&gt; with the &lt;code&gt;auto&lt;/code&gt; matcher fires before automatic compaction and can block (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;). If a routine job hits auto-compaction, it has probably grown larger than you meant it to. Logging it is a cheap signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which hooks catch silent failures
&lt;/h2&gt;

&lt;p&gt;Silent failures show up in four events: &lt;code&gt;PostToolUseFailure&lt;/code&gt;, &lt;code&gt;StopFailure&lt;/code&gt;, &lt;code&gt;Notification&lt;/code&gt; and &lt;code&gt;SessionEnd&lt;/code&gt;. None of them can block. Their job is to tell you something went wrong.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;StopFailure&lt;/code&gt;&lt;/strong&gt; fires when a turn ends because of an API error. Its matcher accepts values like &lt;code&gt;rate_limit&lt;/code&gt;, &lt;code&gt;overloaded&lt;/code&gt;, &lt;code&gt;authentication_failed&lt;/code&gt;, &lt;code&gt;billing_error&lt;/code&gt; and &lt;code&gt;max_output_tokens&lt;/code&gt; (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;). 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/.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PostToolUseFailure&lt;/code&gt;&lt;/strong&gt; fires after any failed tool call. Write those failures to a file so you can check them in the morning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Notification&lt;/code&gt;&lt;/strong&gt; with &lt;code&gt;idle_prompt&lt;/code&gt; or &lt;code&gt;permission_prompt&lt;/code&gt; tells you the run is stuck waiting for a human (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SessionEnd&lt;/code&gt;&lt;/strong&gt; reports the reason the session ended (&lt;code&gt;clear&lt;/code&gt;, &lt;code&gt;logout&lt;/code&gt;, &lt;code&gt;prompt_input_exit&lt;/code&gt;, &lt;code&gt;other&lt;/code&gt;). Keep it quick because of the 1.5-second default budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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 (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;). From v2.1.295 you can set &lt;code&gt;"onFailure": "block"&lt;/code&gt; so a broken safety hook stops the run instead of letting it continue unchecked (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;, &lt;a href="https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;changelog&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up your first lifecycle hook
&lt;/h2&gt;

&lt;p&gt;Start with a project-level &lt;code&gt;Stop&lt;/code&gt; guard plus a &lt;code&gt;StopFailure&lt;/code&gt; logger. Together they cover both runaway loops and quiet failures.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Choose the file.&lt;/strong&gt; Use &lt;code&gt;.claude/settings.json&lt;/code&gt; to share hooks with your repo, &lt;code&gt;.claude/settings.local.json&lt;/code&gt; for hooks only you use (gitignored), or &lt;code&gt;~/.claude/settings.json&lt;/code&gt; for all your projects (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a &lt;code&gt;hooks&lt;/code&gt; block.&lt;/strong&gt; Example:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Stop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${CLAUDE_PROJECT_DIR}/.claude/hooks/stop-guard.sh"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"StopFailure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rate_limit|billing_error|server_error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${CLAUDE_PROJECT_DIR}/.claude/hooks/alert.sh"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write &lt;code&gt;stop-guard.sh&lt;/code&gt;.&lt;/strong&gt; It reads JSON from stdin. As the first thing it does, it exits 0 if &lt;code&gt;stop_hook_active&lt;/code&gt; is true. After that it runs your check, such as tests, and exits 2 with a reason on stderr if the check fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make scripts executable&lt;/strong&gt; with &lt;code&gt;chmod +x .claude/hooks/*.sh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify.&lt;/strong&gt; Type &lt;code&gt;/hooks&lt;/code&gt; in Claude Code to open the read-only hook browser. Select &lt;strong&gt;All events&lt;/strong&gt; to confirm each hook appears with its source (user, project, local, plugin, skill or subagent) (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;). The file watcher normally picks up direct edits to settings files without a restart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with a small prompt&lt;/strong&gt; before you hand the hook to a scheduled run. To schedule it afterwards, see /claude-code-cron-job-setup-run-automations-overnight/.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common mistakes and fixes
&lt;/h2&gt;

&lt;p&gt;Most hook problems come from a short list of misunderstandings.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mistake&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stop hook loops forever&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;stop_hook_active&lt;/code&gt; first and add a hard block counter (&lt;a href="https://github.com/anthropics/claude-code/issues/54360" rel="noopener noreferrer"&gt;issue #54360&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hook exits 1 and nothing happens&lt;/td&gt;
&lt;td&gt;Only exit 2 blocks. Codes 1 and 3+ are non-blocking errors (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matcher on &lt;code&gt;Stop&lt;/code&gt; ignored&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Stop&lt;/code&gt;, &lt;code&gt;UserPromptSubmit&lt;/code&gt; and &lt;code&gt;PostToolBatch&lt;/code&gt; don't support matchers. They always fire (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path breaks with spaces&lt;/td&gt;
&lt;td&gt;Use exec form (&lt;code&gt;command&lt;/code&gt; + &lt;code&gt;args&lt;/code&gt;) with &lt;code&gt;${CLAUDE_PROJECT_DIR}&lt;/code&gt; (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Huge stdout clutters context&lt;/td&gt;
&lt;td&gt;Output over 10,000 characters is saved to a file with a preview, so keep it short (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expecting order between hooks&lt;/td&gt;
&lt;td&gt;Matching hooks run in parallel. Put dependent steps in one script&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you want to undo the &lt;em&gt;file edits&lt;/em&gt; a run made, rather than prevent them, see /claude-code-checkpoints-how-rollback-works-in-automations/.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;You can have useful guards in place today with three small changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do it yourself today:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the &lt;code&gt;Stop&lt;/code&gt; guard and &lt;code&gt;StopFailure&lt;/code&gt; alert from the steps above to one project.&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;PostToolBatch&lt;/code&gt; step counter that returns &lt;code&gt;continue: false&lt;/code&gt; at a limit you choose.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;/hooks&lt;/code&gt;, select &lt;strong&gt;All events&lt;/strong&gt;, and confirm what's actually active, including hooks from plugins.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;If you'd rather not do it yourself:&lt;/strong&gt; 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 &lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;official page&lt;/a&gt;). 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 &lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt; as the spec, and make sure you understand every script before it runs unattended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related guides on this site:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;/why-claude-code-stops-mid-automation-and-how-to-fix-it/&lt;/li&gt;
&lt;li&gt;/claude-code-checkpoints-how-rollback-works-in-automations/&lt;/li&gt;
&lt;li&gt;/claude-code-cron-job-setup-run-automations-overnight/&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the order of Claude Code hook events?
&lt;/h3&gt;

&lt;p&gt;A typical run goes &lt;code&gt;SessionStart&lt;/code&gt; → &lt;code&gt;UserPromptSubmit&lt;/code&gt; → &lt;code&gt;PreToolUse&lt;/code&gt; → &lt;code&gt;PostToolUse&lt;/code&gt;/&lt;code&gt;PostToolUseFailure&lt;/code&gt; → &lt;code&gt;PostToolBatch&lt;/code&gt;, and the tool steps repeat until &lt;code&gt;Stop&lt;/code&gt; or &lt;code&gt;StopFailure&lt;/code&gt;. &lt;code&gt;SessionEnd&lt;/code&gt; fires when the session closes. Compaction, subagent and notification events fire in between as needed (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Which Claude Code hook can block an action?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PreToolUse&lt;/code&gt;, &lt;code&gt;UserPromptSubmit&lt;/code&gt;, &lt;code&gt;PostToolBatch&lt;/code&gt;, &lt;code&gt;Stop&lt;/code&gt;, &lt;code&gt;SubagentStop&lt;/code&gt;, &lt;code&gt;PreCompact&lt;/code&gt;, &lt;code&gt;ConfigChange&lt;/code&gt; and several task, worktree and elicitation events can block with exit code 2 or a JSON decision. &lt;code&gt;SessionStart&lt;/code&gt;, &lt;code&gt;Notification&lt;/code&gt;, &lt;code&gt;StopFailure&lt;/code&gt; and &lt;code&gt;SessionEnd&lt;/code&gt; can't (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I stop a Claude Code Stop hook from looping?
&lt;/h3&gt;

&lt;p&gt;Read &lt;code&gt;stop_hook_active&lt;/code&gt; from the hook's stdin and exit 0 when it's &lt;code&gt;true&lt;/code&gt;. Because of a reported propagation bug, also keep a per-session counter and allow the stop after a few blocks (&lt;a href="https://github.com/anthropics/claude-code/issues/54360" rel="noopener noreferrer"&gt;issue #54360&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Where do I put Claude Code hooks?
&lt;/h3&gt;

&lt;p&gt;Put them in &lt;code&gt;~/.claude/settings.json&lt;/code&gt; (all projects), &lt;code&gt;.claude/settings.json&lt;/code&gt; (shared project), &lt;code&gt;.claude/settings.local.json&lt;/code&gt; (personal project), plugin &lt;code&gt;hooks/hooks.json&lt;/code&gt;, or skill and subagent frontmatter. Hooks from all of these locations run together (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Do prompt or agent hooks cost extra?
&lt;/h3&gt;

&lt;p&gt;They use model calls. &lt;code&gt;prompt&lt;/code&gt; hooks send text to a model, and &lt;code&gt;agent&lt;/code&gt; hooks start a subagent (marked experimental), so they count toward your plan's usage (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;reference&lt;/a&gt;, &lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;). Use command hooks for checks that don't need judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference – Claude Code Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener noreferrer"&gt;Automate actions with hooks – Claude Code Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/agent-sdk/hooks" rel="noopener noreferrer"&gt;Intercept and control agent behavior with hooks – Agent SDK Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;Claude Code changelog – GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;Claude pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/54360" rel="noopener noreferrer"&gt;GitHub issue #54360: Stop hook fires repeatedly (stop_hook_active not propagating)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://amitkoth.com/claude-code-stop-hooks/" rel="noopener noreferrer"&gt;How Claude Code stop hooks work – Amit Kothari&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://claudefa.st/blog/tools/hooks/stop-hook-task-enforcement" rel="noopener noreferrer"&gt;Claude Code Stop Hook: Force Task Completion – claudefa.st&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was created with the help of AI. Sources are linked inline; check the official documentation for the latest details.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>hooks</category>
      <category>automation</category>
      <category>aiagents</category>
    </item>
    <item>
      <title>Claude Code Hooks Examples: 7 Copy-Paste Scripts</title>
      <dc:creator>Aoba Works</dc:creator>
      <pubDate>Sat, 17 Oct 2026 15:43:19 +0000</pubDate>
      <link>https://dev.to/aobaworks/claude-code-hooks-examples-7-copy-paste-scripts-2clg</link>
      <guid>https://dev.to/aobaworks/claude-code-hooks-examples-7-copy-paste-scripts-2clg</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://en.aobaworks.com/claude-code-hooks-examples-7-copy-paste-scripts/?utm_source=devto&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unattended&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Claude Code hooks are shell commands that run at fixed points in a session. Common ones run before a tool call (&lt;code&gt;PreToolUse&lt;/code&gt;), after one (&lt;code&gt;PostToolUse&lt;/code&gt;), when Claude tries to finish (&lt;code&gt;Stop&lt;/code&gt;) and when a session ends (&lt;code&gt;SessionEnd&lt;/code&gt;). 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 &lt;code&gt;.claude/hooks/&lt;/code&gt;, wired up in &lt;code&gt;.claude/settings.json&lt;/code&gt; (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Which option is right for you?
&lt;/h2&gt;

&lt;p&gt;Start with the one hook that fixes the problem you have today, then add others as you need them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;What to pick first&lt;/th&gt;
&lt;th&gt;First page to open&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Beginner, first hook ever&lt;/td&gt;
&lt;td&gt;Script 4 (notification when Claude is waiting). It changes nothing and is easy to check&lt;/td&gt;
&lt;td&gt;&lt;a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener noreferrer"&gt;Hooks guide&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily work, Claude runs commands unattended&lt;/td&gt;
&lt;td&gt;Scripts 1 + 2 (guardrails)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference: PreToolUse&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overnight or cron runs that stop early&lt;/td&gt;
&lt;td&gt;Script 3 (Stop test gate) + Script 5 (tool log)&lt;/td&gt;
&lt;td&gt;/claude-code-cron-job-setup-run-automations-overnight/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Watching your budget or usage&lt;/td&gt;
&lt;td&gt;Scripts 5 + 6 (logging)&lt;/td&gt;
&lt;td&gt;/claude-code-how-to-see-token-usage-and-reduce-it/&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why these picks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Beginner:&lt;/strong&gt; a notification hook can't block anything, so if it's wrong, nothing breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily use:&lt;/strong&gt; guardrails are deterministic. They always run, so you don't depend on the model remembering a rule (&lt;a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener noreferrer"&gt;Hooks guide&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget:&lt;/strong&gt; logs show you what the agent actually did before you change plans or prompts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you haven't read about the events yet, start with /claude-code-hooks-lifecycle-every-event-explained/.&lt;/p&gt;

&lt;h2&gt;
  
  
  How hooks are wired: the 60-second version
&lt;/h2&gt;

&lt;p&gt;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 (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Key facts from the official reference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Where:&lt;/strong&gt; &lt;code&gt;~/.claude/settings.json&lt;/code&gt; (all your projects), &lt;code&gt;.claude/settings.json&lt;/code&gt; (project, can be committed) or &lt;code&gt;.claude/settings.local.json&lt;/code&gt; (project, gitignored).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input on stdin:&lt;/strong&gt; &lt;code&gt;session_id&lt;/code&gt;, &lt;code&gt;transcript_path&lt;/code&gt;, &lt;code&gt;cwd&lt;/code&gt;, &lt;code&gt;permission_mode&lt;/code&gt;, &lt;code&gt;hook_event_name&lt;/code&gt;, plus event fields like &lt;code&gt;tool_name&lt;/code&gt; and &lt;code&gt;tool_input&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit codes:&lt;/strong&gt; &lt;code&gt;0&lt;/code&gt; = success, and any JSON output is applied. &lt;code&gt;2&lt;/code&gt; = blocking error on events that support blocking. Any other code = non-blocking error, and the action still goes ahead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout:&lt;/strong&gt; 600 seconds by default for command hooks. You can change it with &lt;code&gt;"timeout"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matcher:&lt;/strong&gt; &lt;code&gt;"Bash"&lt;/code&gt;, &lt;code&gt;"Edit|Write"&lt;/code&gt;, or a regex. Leave it empty or use &lt;code&gt;"*"&lt;/code&gt; to match everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paths:&lt;/strong&gt; use &lt;code&gt;${CLAUDE_PROJECT_DIR}&lt;/code&gt; so scripts work from any working directory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All scripts below need &lt;a href="https://jqlang.org/" rel="noopener noreferrer"&gt;&lt;code&gt;jq&lt;/code&gt;&lt;/a&gt; to parse the JSON on stdin.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup steps (do this once)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In your project root, run &lt;code&gt;mkdir -p .claude/hooks .claude/logs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Save each script below into &lt;code&gt;.claude/hooks/&lt;/code&gt; and make it executable: &lt;code&gt;chmod +x .claude/hooks/*.sh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add the matching JSON block to &lt;code&gt;.claude/settings.json&lt;/code&gt; under the top-level &lt;code&gt;"hooks"&lt;/code&gt; key. If you use several hooks, merge them into one &lt;code&gt;"hooks"&lt;/code&gt; object.&lt;/li&gt;
&lt;li&gt;In Claude Code, type &lt;strong&gt;&lt;code&gt;/hooks&lt;/code&gt;&lt;/strong&gt;. The menu is a read-only browser that shows each hook, its event and which file it came from (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Trigger the event on purpose (for example, ask Claude to run &lt;code&gt;rm -rf /tmp/x&lt;/code&gt;) and check that the hook fires.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Guardrail scripts (1–2): stop damage before it happens
&lt;/h2&gt;

&lt;p&gt;Guardrails use &lt;code&gt;PreToolUse&lt;/code&gt;, which runs before a tool call and can allow, deny, ask or defer it (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Script 1: Block destructive shell commands
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.claude/hooks/block-dangerous.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Example guardrail: deny risky Bash commands before they run.&lt;/span&gt;
&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_input.command // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CMD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Eq&lt;/span&gt; &lt;span class="s1"&gt;'rm -rf|git push --force|git reset --hard|DROP TABLE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; c &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CMD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: ("Blocked by guardrail hook: " + $c)
    }
  }'&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${CLAUDE_PROJECT_DIR}/.claude/hooks/block-dangerous.sh"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;permissionDecision: "deny"&lt;/code&gt; output is the documented way to block a call. If the script prints nothing and exits 0, the normal permission flow applies (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;). &lt;strong&gt;Limitation:&lt;/strong&gt; pattern matching is easy to get around (for example, &lt;code&gt;rm -r -f&lt;/code&gt;). Treat it as a seatbelt, not a sandbox, and keep your normal &lt;a href="https://code.claude.com/docs/en/permissions" rel="noopener noreferrer"&gt;permission rules&lt;/a&gt; too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Script 2: Protect secrets and lock files
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.claude/hooks/protect-files.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Example: refuse edits to sensitive paths. Exit 2 = block; stderr is the reason.&lt;/span&gt;
&lt;span class="nv"&gt;FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_input.file_path // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;p &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;".env"&lt;/span&gt; &lt;span class="s2"&gt;"package-lock.json"&lt;/span&gt; &lt;span class="s2"&gt;".git/"&lt;/span&gt; &lt;span class="s2"&gt;"secrets/"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Edits to &lt;/span&gt;&lt;span class="nv"&gt;$FILE&lt;/span&gt;&lt;span class="s2"&gt; are blocked (protected path: &lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;)."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;exit &lt;/span&gt;2
  &lt;span class="k"&gt;fi
done
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Matcher: &lt;code&gt;"Edit|Write"&lt;/code&gt;. On &lt;code&gt;PreToolUse&lt;/code&gt;, 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 (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Stall-prevention scripts (3–5): keep unattended runs moving
&lt;/h2&gt;

&lt;p&gt;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/.&lt;/p&gt;

&lt;h3&gt;
  
  
  Script 3: Don't let Claude stop while tests fail
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.claude/hooks/stop-gate.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Example Stop gate: block "done" once if tests fail.&lt;/span&gt;
&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;# Loop guard: if we already forced a continuation, let Claude stop.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.stop_hook_active // false'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLAUDE_PROJECT_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .claude/logs/last-test.log 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'{decision: "block",
          reason: "Tests are failing. Read .claude/logs/last-test.log, fix the failures, then finish."}'&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"Stop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${CLAUDE_PROJECT_DIR}/.claude/hooks/stop-gate.sh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On &lt;code&gt;Stop&lt;/code&gt;, &lt;code&gt;decision: "block"&lt;/code&gt; with a &lt;code&gt;reason&lt;/code&gt; keeps Claude working, and the reason becomes its next instruction (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;). The &lt;code&gt;stop_hook_active&lt;/code&gt; field is &lt;code&gt;true&lt;/code&gt; when Claude is already continuing because of a Stop hook. Check it, or you can create a loop that never ends (&lt;a href="https://www.cubic.dev/blog/claude-code-hooks" rel="noopener noreferrer"&gt;cubic&lt;/a&gt;, &lt;a href="https://claudefa.st/blog/tools/hooks/stop-hook-task-enforcement" rel="noopener noreferrer"&gt;claudefa.st&lt;/a&gt;). One third-party write-up says Claude Code ends the turn itself after eight blocks in a row (&lt;a href="https://www.cubic.dev/blog/claude-code-hooks" rel="noopener noreferrer"&gt;cubic&lt;/a&gt;). Don't rely on that. &lt;strong&gt;Not for:&lt;/strong&gt; projects whose test suite takes longer than your timeout. Swap in a faster check such as lint or type-check.&lt;/p&gt;

&lt;h3&gt;
  
  
  Script 4: Get pinged when Claude is waiting for you
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.claude/hooks/notify.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Example: log + desktop alert when Claude needs input.&lt;/span&gt;
&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.message // "Claude Code needs your input"'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%TZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLAUDE_PROJECT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/.claude/logs/waiting.log"&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; osascript &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;osascript &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"display notification &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; with title &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Claude Code&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;elif &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; notify-send &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;notify-send &lt;span class="s2"&gt;"Claude Code"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put it under the &lt;code&gt;Notification&lt;/code&gt; event with matcher &lt;code&gt;"permission_prompt|idle_prompt"&lt;/code&gt;. Both are documented notification types (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;). On a headless server the desktop alert does nothing, but &lt;code&gt;waiting.log&lt;/code&gt; still shows you where an overnight run got stuck.&lt;/p&gt;

&lt;h3&gt;
  
  
  Script 5 (bonus context): Brief Claude at session start
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.claude/hooks/session-context.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Example: give Claude the current repo state at startup.&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLAUDE_PROJECT_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Branch: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git branch &lt;span class="nt"&gt;--show-current&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Uncommitted files:"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; git status &lt;span class="nt"&gt;--short&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; TODO.md &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Open TODOs:"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt; TODO.md&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On &lt;code&gt;SessionStart&lt;/code&gt;, plain stdout is added to Claude's context. That's different from most events, where it goes to the debug log (&lt;a href="https://www.cubic.dev/blog/claude-code-hooks" rel="noopener noreferrer"&gt;cubic&lt;/a&gt;). Keep it short: stdout fields are capped at 10,000 characters, and anything longer is saved to a file with only a preview shown (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage-logging scripts (6–7): know what the agent actually did
&lt;/h2&gt;

&lt;p&gt;Logging hooks add one JSON line per event to a file, so you can review a run afterwards without reading the whole transcript.&lt;/p&gt;

&lt;h3&gt;
  
  
  Script 6: Log every tool call
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.claude/hooks/log-tools.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Example: one JSON line per tool call.&lt;/span&gt;
jq &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'{ts: (now|todate), session: .session_id, tool: .tool_name,
        target: (.tool_input.command // .tool_input.file_path // .tool_input.url // null)}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLAUDE_PROJECT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/.claude/logs/tools.jsonl"&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${CLAUDE_PROJECT_DIR}/.claude/hooks/log-tools.sh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"async"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;"async": true&lt;/code&gt; is a documented handler option (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;). Use it so logging doesn't slow each tool call down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Script 7: Session summary on exit
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.claude/hooks/session-summary.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Example: summarize a session when it ends.&lt;/span&gt;
&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;SID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.session_id'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;REASON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.reason // "unknown"'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLAUDE_PROJECT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/.claude/logs/tools.jsonl"&lt;/span&gt;
&lt;span class="nv"&gt;COUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$SID&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;0&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;TOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$SID&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%TZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; session=&lt;/span&gt;&lt;span class="nv"&gt;$SID&lt;/span&gt;&lt;span class="s2"&gt; reason=&lt;/span&gt;&lt;span class="nv"&gt;$REASON&lt;/span&gt;&lt;span class="s2"&gt; tool_calls=&lt;/span&gt;&lt;span class="nv"&gt;$COUNT&lt;/span&gt;&lt;span class="s2"&gt; top=[&lt;/span&gt;&lt;span class="nv"&gt;$TOP&lt;/span&gt;&lt;span class="s2"&gt;] transcript=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.transcript_path'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLAUDE_PROJECT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/.claude/logs/sessions.log"&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register it under &lt;code&gt;SessionEnd&lt;/code&gt;. Documented end reasons include &lt;code&gt;clear&lt;/code&gt;, &lt;code&gt;logout&lt;/code&gt;, &lt;code&gt;prompt_input_exit&lt;/code&gt; and &lt;code&gt;other&lt;/code&gt; (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;). &lt;strong&gt;Limitation:&lt;/strong&gt; 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/.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes and fixes
&lt;/h2&gt;

&lt;p&gt;Most broken hooks come down to one of five problems:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hook never fires&lt;/td&gt;
&lt;td&gt;Wrong event name or matcher (names are case-sensitive, e.g. &lt;code&gt;Bash&lt;/code&gt;, `Edit\&lt;/td&gt;
&lt;td&gt;Write`)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"hook error" notice&lt;/td&gt;
&lt;td&gt;Script isn't executable or &lt;code&gt;jq&lt;/code&gt; is missing&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;chmod +x&lt;/code&gt;, install &lt;code&gt;jq&lt;/code&gt;, and test with `echo '{}' \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON output ignored&lt;/td&gt;
&lt;td&gt;Exited with a code other than 0, or extra text was printed before the JSON&lt;/td&gt;
&lt;td&gt;Print only the JSON, then {% raw %}&lt;code&gt;exit 0&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude never finishes&lt;/td&gt;
&lt;td&gt;Stop hook ignores &lt;code&gt;stop_hook_active&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Add the loop guard from Script 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works in one folder, not another&lt;/td&gt;
&lt;td&gt;Relative paths&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;${CLAUDE_PROJECT_DIR}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For anything else, turn on debug logging. It shows hook runs, full stderr and JSON parse failures (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;, &lt;a href="https://code.claude.com/docs/en/debug-logs" rel="noopener noreferrer"&gt;debug logs&lt;/a&gt;). 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;The fastest win is one guardrail and one log today, then a test gate once both are working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Today, on your own:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add Script 1 and Script 6 to one project, then use &lt;code&gt;/hooks&lt;/code&gt; to check both are registered.&lt;/li&gt;
&lt;li&gt;Run a normal task and read &lt;code&gt;.claude/logs/tools.jsonl&lt;/code&gt; afterwards.&lt;/li&gt;
&lt;li&gt;When it looks right, add Script 3 with your fastest test command.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;If you'd rather not build this yourself:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Academy's "Claude Code in Action"&lt;/strong&gt;: a free course of about one hour and 9 lessons, including using hooks to enforce rules (&lt;a href="https://anthropic.skilljar.com/claude-code-in-action" rel="noopener noreferrer"&gt;Skilljar&lt;/a&gt;, &lt;a href="https://www.classcentral.com/course/anthropic-academy-claude-code-in-action-536160" rel="noopener noreferrer"&gt;Class Central&lt;/a&gt;). Good if you want structured basics. It isn't for you if you need help with your specific setup, because it's general training.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Team plan&lt;/strong&gt;: $25/seat/month, or $20 billed annually (as of October 2026; check the &lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;official page&lt;/a&gt;). Useful if a small team wants shared project settings, and admins can restrict hooks with managed settings such as &lt;code&gt;allowManagedHooksOnly&lt;/code&gt; (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;). Not worth it for a solo builder: Pro ($20/month, or $17 billed annually, as of October 2026; see &lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;Claude pricing&lt;/a&gt;) already includes Claude Code. &lt;strong&gt;Limitation:&lt;/strong&gt; no plan writes your hooks for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Related guides on this site:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;/claude-code-hooks-lifecycle-every-event-explained/&lt;/li&gt;
&lt;li&gt;/why-claude-code-stops-mid-automation-and-how-to-fix-it/&lt;/li&gt;
&lt;li&gt;/claude-code-cron-job-setup-run-automations-overnight/&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Where do I put Claude Code hooks?
&lt;/h3&gt;

&lt;p&gt;Put them in &lt;code&gt;.claude/settings.json&lt;/code&gt; for a project you share, &lt;code&gt;.claude/settings.local.json&lt;/code&gt; for personal project hooks, or &lt;code&gt;~/.claude/settings.json&lt;/code&gt; for all your projects. The scripts themselves can live anywhere, but &lt;code&gt;.claude/hooks/&lt;/code&gt; plus &lt;code&gt;${CLAUDE_PROJECT_DIR}&lt;/code&gt; keeps paths reliable (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I block a command with a hook?
&lt;/h3&gt;

&lt;p&gt;Use a &lt;code&gt;PreToolUse&lt;/code&gt; hook that either prints JSON with &lt;code&gt;permissionDecision: "deny"&lt;/code&gt; or exits with code 2 and a reason on stderr. Both stop the tool call, and Claude sees the reason (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does my Stop hook make Claude loop forever?
&lt;/h3&gt;

&lt;p&gt;The hook blocks every stop, even after Claude has already been forced to continue. Read &lt;code&gt;stop_hook_active&lt;/code&gt; from stdin and exit 0 when it's &lt;code&gt;true&lt;/code&gt; (&lt;a href="https://www.cubic.dev/blog/claude-code-hooks" rel="noopener noreferrer"&gt;cubic&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Do hooks slow Claude Code down?
&lt;/h3&gt;

&lt;p&gt;Synchronous hooks run inline, and command hooks can run for up to 600 seconds by default. Keep checks fast, set a lower &lt;code&gt;timeout&lt;/code&gt;, and mark logging hooks &lt;code&gt;"async": true&lt;/code&gt; (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference – Claude Code Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener noreferrer"&gt;Automate actions with hooks (Hooks guide) – Claude Code Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/permissions" rel="noopener noreferrer"&gt;Permissions – Claude Code Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/debug-logs" rel="noopener noreferrer"&gt;Debug logs – Claude Code Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cubic.dev/blog/claude-code-hooks" rel="noopener noreferrer"&gt;Claude Code hooks: every event, with examples – cubic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://claudefa.st/blog/tools/hooks/stop-hook-task-enforcement" rel="noopener noreferrer"&gt;Claude Code Stop Hook: Force Task Completion – claudefa.st&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;Claude pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://anthropic.skilljar.com/claude-code-in-action" rel="noopener noreferrer"&gt;Claude Code in Action – Anthropic Courses&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.classcentral.com/course/anthropic-academy-claude-code-in-action-536160" rel="noopener noreferrer"&gt;Claude Code in Action – Class Central&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jqlang.org/" rel="noopener noreferrer"&gt;jq&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was created with the help of AI. Sources are linked inline; check the official documentation for the latest details.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>hooks</category>
      <category>automation</category>
      <category>guardrails</category>
    </item>
    <item>
      <title>Claude Code: How to See Token Usage (and Reduce It)</title>
      <dc:creator>Aoba Works</dc:creator>
      <pubDate>Sat, 17 Oct 2026 11:31:21 +0000</pubDate>
      <link>https://dev.to/aobaworks/claude-code-how-to-see-token-usage-and-reduce-it-2kko</link>
      <guid>https://dev.to/aobaworks/claude-code-how-to-see-token-usage-and-reduce-it-2kko</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://en.aobaworks.com/claude-code-how-to-see-token-usage-and-reduce-it/?utm_source=devto&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unattended&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To see token usage in Claude Code, type &lt;code&gt;/usage&lt;/code&gt; in an interactive session. The Session block lists your input, output and cache tokens per model, plus an estimated dollar total (&lt;a href="https://code.claude.com/docs/en/costs" rel="noopener noreferrer"&gt;Claude Code docs: Manage costs&lt;/a&gt;). For headless runs, add &lt;code&gt;--output-format json&lt;/code&gt; to &lt;code&gt;claude -p&lt;/code&gt;. The result then includes &lt;code&gt;total_cost_usd&lt;/code&gt; and a cost breakdown per model, so a script can log every run (&lt;a href="https://code.claude.com/docs/en/headless" rel="noopener noreferrer"&gt;Run Claude Code programmatically&lt;/a&gt;). For the real bill on an API key, check the Usage page in the &lt;a href="https://platform.claude.com/usage" rel="noopener noreferrer"&gt;Claude Console&lt;/a&gt;. Claude Code's own figures are client-side estimates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which option is right for you?
&lt;/h2&gt;

&lt;p&gt;The right place to check depends on how you pay and whether anyone watches the run.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;What to pick first&lt;/th&gt;
&lt;th&gt;First page to open&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Beginner, interactive sessions only&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/usage&lt;/code&gt; inside Claude Code&lt;/td&gt;
&lt;td&gt;&lt;a href="https://code.claude.com/docs/en/costs" rel="noopener noreferrer"&gt;Manage costs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily work on a Pro/Max subscription&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/usage&lt;/code&gt; plan bars plus the 24h/7d breakdown&lt;/td&gt;
&lt;td&gt;&lt;a href="https://code.claude.com/docs/en/costs" rel="noopener noreferrer"&gt;Manage costs → Plan usage breakdown&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recurring headless jobs (cron, CI)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--output-format json&lt;/code&gt; + &lt;code&gt;--max-budget-usd&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://code.claude.com/docs/en/headless" rel="noopener noreferrer"&gt;Run Claude Code programmatically&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tight budget on an API key&lt;/td&gt;
&lt;td&gt;Workspace &lt;strong&gt;Spend limits&lt;/strong&gt; in the Console&lt;/td&gt;
&lt;td&gt;&lt;a href="https://platform.claude.com/docs/en/build-with-claude/workspaces" rel="noopener noreferrer"&gt;Workspaces&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Want numbers for a dashboard&lt;/td&gt;
&lt;td&gt;Usage and Cost Admin API or OpenTelemetry&lt;/td&gt;
&lt;td&gt;&lt;a href="https://platform.claude.com/docs/en/build-with-claude/usage-cost-api" rel="noopener noreferrer"&gt;Usage and Cost API&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Beginner:&lt;/strong&gt; &lt;code&gt;/usage&lt;/code&gt; needs no setup. It is the quickest way to learn what a normal session costs you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily work:&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget:&lt;/strong&gt; a spend limit in the Console is enforced by Anthropic. Local estimates can't cap anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to read /usage (and where /cost went)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/usage&lt;/code&gt; is the in-session command for token and cost numbers. Current docs describe everything under it.&lt;/p&gt;

&lt;p&gt;The changelog still names &lt;code&gt;/cost&lt;/code&gt; in recent entries, such as the v2.1.296 note about cache-read pricing (&lt;a href="https://code.claude.com/docs/en/changelog" rel="noopener noreferrer"&gt;changelog&lt;/a&gt;). If &lt;code&gt;/cost&lt;/code&gt; doesn't do what an older tutorial says, use &lt;code&gt;/usage&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Start a session with &lt;code&gt;claude&lt;/code&gt; and do some work.&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;/usage&lt;/code&gt; and press Enter. It runs right away, even while Claude is responding (&lt;a href="https://code.claude.com/docs/en/commands" rel="noopener noreferrer"&gt;Commands&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Read the &lt;strong&gt;Session&lt;/strong&gt; block: &lt;code&gt;Total cost&lt;/code&gt;, &lt;code&gt;Total duration (API)&lt;/code&gt;, &lt;code&gt;Total duration (wall)&lt;/code&gt;, &lt;code&gt;Total code changes&lt;/code&gt;, and &lt;strong&gt;Usage by model&lt;/strong&gt;. Usage by model shows input, output, cache-read and cache-write tokens for each model (&lt;a href="https://code.claude.com/docs/en/costs" rel="noopener noreferrer"&gt;Manage costs&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;On v2.1.251 or later, check the &lt;strong&gt;Prompt cache (main)&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;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 &lt;code&gt;d&lt;/code&gt; or &lt;code&gt;w&lt;/code&gt; to switch between 24 hours and 7 days.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What the numbers mean
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The dollar figure is calculated &lt;strong&gt;locally at list price&lt;/strong&gt;. It is an estimate, not an invoice (&lt;a href="https://code.claude.com/docs/en/costs" rel="noopener noreferrer"&gt;Manage costs&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;On a subscription, the Session cost line doesn't matter for billing. The plan bars do.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/usage&lt;/code&gt; reads local session history only. Usage from other machines or claude.ai is not included.&lt;/li&gt;
&lt;li&gt;The token count next to the spinner (for example &lt;code&gt;↓ 5.5k tokens&lt;/code&gt;) counts only the output of the current turn. It will never match the session totals.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Common mistakes and fixes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"My total went back to $0."&lt;/strong&gt; &lt;code&gt;/clear&lt;/code&gt; starts a new session and resets the Session block (v2.1.211 and later). Note the number before you clear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"&lt;code&gt;/usage&lt;/code&gt; shows old bars."&lt;/strong&gt; If the usage endpoint is rate-limited, you'll see &lt;code&gt;Showing last-known usage&lt;/code&gt;. Press &lt;code&gt;r&lt;/code&gt; to retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"The cost doesn't match my bill."&lt;/strong&gt; That's expected. Use the Console Usage page for the real figure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to track token burn in headless runs
&lt;/h2&gt;

&lt;p&gt;For unattended work, ask for JSON output on every &lt;code&gt;claude -p&lt;/code&gt; call and keep a log of &lt;code&gt;total_cost_usd&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Add &lt;code&gt;--output-format json&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Summarize open TODOs in src/"&lt;/span&gt; &lt;span class="nt"&gt;--output-format&lt;/span&gt; json | jq &lt;span class="s1"&gt;'.total_cost_usd'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payload contains &lt;code&gt;result&lt;/code&gt;, &lt;code&gt;session_id&lt;/code&gt;, &lt;code&gt;total_cost_usd&lt;/code&gt; and a per-model cost breakdown (&lt;a href="https://code.claude.com/docs/en/headless" rel="noopener noreferrer"&gt;headless docs&lt;/a&gt;).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a hard stop with &lt;code&gt;--max-budget-usd 5.00&lt;/code&gt;. The run stops once &lt;em&gt;estimated&lt;/em&gt; spend reaches the cap, and spend from subagents counts toward it. Spend can go past the cap, so leave some headroom (&lt;a href="https://code.claude.com/docs/en/cli-reference" rel="noopener noreferrer"&gt;CLI reference&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;--max-turns&lt;/code&gt; to stop runaway loops. When the limit is hit, the run exits with an error.&lt;/li&gt;
&lt;li&gt;In CI, add &lt;code&gt;--bare&lt;/code&gt;. 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 &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; (&lt;a href="https://code.claude.com/docs/en/headless" rel="noopener noreferrer"&gt;headless docs&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Example: a cost-logging wrapper
&lt;/h3&gt;

&lt;p&gt;This is an illustrative script, not an official tool. Change the paths and limits to suit your setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# run-claude.sh "&amp;lt;prompt&amp;gt;"&lt;/span&gt;
&lt;span class="nv"&gt;ALERT_AT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.00
&lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--output-format&lt;/span&gt; json &lt;span class="nt"&gt;--max-budget-usd&lt;/span&gt; 3.00 &lt;span class="nt"&gt;--max-turns&lt;/span&gt; 30&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="nv"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.total_cost_usd // 0'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;sid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.session_id // "none"'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%TZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="nv"&gt;$sid&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="nv"&gt;$cost&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/claude-costs.csv"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cost&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$ALERT_AT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | bc &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Claude run &lt;/span&gt;&lt;span class="nv"&gt;$sid&lt;/span&gt;&lt;span class="s2"&gt; cost &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="nv"&gt;$cost&lt;/span&gt;&lt;span class="s2"&gt; (alert at &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="nv"&gt;$ALERT_AT&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="c"&gt;# send to a channel you own, e.g. curl -X POST "$YOUR_WEBHOOK_URL" -d "..."&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script still logs when the run fails, because &lt;code&gt;|| status=$?&lt;/code&gt; stops a non-zero exit from killing it. Failed runs still use tokens, and the SDK docs note that error results carry cost too (&lt;a href="https://code.claude.com/docs/en/agent-sdk/cost-tracking" rel="noopener noreferrer"&gt;Track cost and usage&lt;/a&gt;). To schedule the script, see &lt;a href="https://en.aobaworks.com/claude-code-cron-job-setup-run-automations-overnight/" rel="noopener noreferrer"&gt;Claude Code Cron Job Setup: Run Automations Overnight&lt;/a&gt;. If runs stop early because of &lt;code&gt;--max-turns&lt;/code&gt;, read &lt;a href="https://en.aobaworks.com/why-claude-code-stops-mid-automation-and-how-to-fix-it/" rel="noopener noreferrer"&gt;Why Claude Code Stops Mid-Automation (and How to Fix It)&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common mistakes and fixes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Summing totals across &lt;code&gt;--resume&lt;/code&gt; runs.&lt;/strong&gt; A resumed run reports the whole conversation's total, including earlier runs (&lt;a href="https://code.claude.com/docs/en/headless" rel="noopener noreferrer"&gt;headless docs&lt;/a&gt;). Take the latest value. Don't add them up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting &lt;code&gt;usage&lt;/code&gt; for subagent-heavy jobs.&lt;/strong&gt; The &lt;code&gt;usage&lt;/code&gt; field leaves out subagent tokens. &lt;code&gt;total_cost_usd&lt;/code&gt; and &lt;code&gt;modelUsage&lt;/code&gt; include them (&lt;a href="https://code.claude.com/docs/en/agent-sdk/cost-tracking" rel="noopener noreferrer"&gt;Track cost and usage&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing customers from these fields.&lt;/strong&gt; Anthropic says not to. They are client-side estimates.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to wire usage checks into hooks
&lt;/h2&gt;

&lt;p&gt;Hooks are good for cutting tokens, but they can't produce a per-run cost alert on their own. According to the &lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;hooks reference&lt;/a&gt;, &lt;code&gt;Stop&lt;/code&gt; and &lt;code&gt;SessionEnd&lt;/code&gt; hooks receive &lt;code&gt;session_id&lt;/code&gt;, &lt;code&gt;transcript_path&lt;/code&gt;, &lt;code&gt;cwd&lt;/code&gt; and a &lt;code&gt;reason&lt;/code&gt;, but no token or cost data.&lt;/p&gt;

&lt;p&gt;What does work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Alerting:&lt;/strong&gt; put the cost check in the wrapper script above, which runs after the &lt;code&gt;-p&lt;/code&gt; process exits. The cost figure exists there and nowhere else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resume warnings:&lt;/strong&gt; per the hooks reference, a &lt;code&gt;SessionStart&lt;/code&gt; hook that fires on resume receives &lt;code&gt;context_tokens&lt;/code&gt;, &lt;code&gt;estimated_cache_write_usd&lt;/code&gt; and &lt;code&gt;prompt_cache_likely_expired&lt;/code&gt;. You can log these, or warn yourself before an expensive cold-cache resume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cutting tokens with PreToolUse:&lt;/strong&gt; the costs page includes an official &lt;code&gt;PreToolUse&lt;/code&gt; hook on &lt;code&gt;Bash&lt;/code&gt;. It rewrites &lt;code&gt;npm test&lt;/code&gt;, &lt;code&gt;pytest&lt;/code&gt; or &lt;code&gt;go test&lt;/code&gt; 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" (&lt;a href="https://code.claude.com/docs/en/costs" rel="noopener noreferrer"&gt;Manage costs&lt;/a&gt;). To check it's active, run &lt;code&gt;/hooks&lt;/code&gt; and look under &lt;strong&gt;PreToolUse&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live view:&lt;/strong&gt; a custom status line receives session cost data and can show it all the time (&lt;a href="https://code.claude.com/docs/en/statusline" rel="noopener noreferrer"&gt;Status line&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a map of every hook event and when each one fires, see &lt;a href="https://en.aobaworks.com/claude-code-hooks-lifecycle-every-event-explained/" rel="noopener noreferrer"&gt;Claude Code Hooks Lifecycle: Every Event, Explained&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; &lt;code&gt;--bare&lt;/code&gt; skips hooks. If your CI runs use bare mode, a PreToolUse filter in &lt;code&gt;~/.claude&lt;/code&gt; won't run. Pass the settings with &lt;code&gt;--settings&lt;/code&gt; instead (&lt;a href="https://code.claude.com/docs/en/headless" rel="noopener noreferrer"&gt;headless docs&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to see real billing: Console and APIs
&lt;/h2&gt;

&lt;p&gt;For money you are actually charged on an API key, the Claude Console is the source of truth.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Usage page:&lt;/strong&gt; go to &lt;a href="https://platform.claude.com/usage" rel="noopener noreferrer"&gt;platform.claude.com/usage&lt;/a&gt; for usage over time. When you first sign in to Claude Code with a Console account, a &lt;strong&gt;Claude Code&lt;/strong&gt; workspace is created automatically to keep that traffic separate (&lt;a href="https://platform.claude.com/docs/en/build-with-claude/workspaces" rel="noopener noreferrer"&gt;Workspaces&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spend limits:&lt;/strong&gt; open &lt;strong&gt;Settings → Workspaces&lt;/strong&gt;, select the workspace, then click &lt;strong&gt;Spend limits&lt;/strong&gt;. This caps monthly spend and can alert you at thresholds you choose. You can't set limits on the Default Workspace (&lt;a href="https://platform.claude.com/docs/en/build-with-claude/workspaces" rel="noopener noreferrer"&gt;Workspaces&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits:&lt;/strong&gt; on the &lt;strong&gt;Rate limits&lt;/strong&gt; page, choose the workspace from the &lt;strong&gt;Workspace&lt;/strong&gt; dropdown and click &lt;strong&gt;Edit&lt;/strong&gt; next to a model (&lt;a href="https://platform.claude.com/docs/en/build-with-claude/workspaces" rel="noopener noreferrer"&gt;Workspaces&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usage and Cost Admin API:&lt;/strong&gt; &lt;code&gt;/v1/organizations/usage_report/messages&lt;/code&gt; (buckets of &lt;code&gt;1m&lt;/code&gt;, &lt;code&gt;1h&lt;/code&gt; or &lt;code&gt;1d&lt;/code&gt;) and &lt;code&gt;/v1/organizations/cost_report&lt;/code&gt; (daily). Both need an Admin API key, and &lt;strong&gt;the Admin API is not available to individual accounts&lt;/strong&gt;. Data usually appears within 5 minutes (&lt;a href="https://platform.claude.com/docs/en/build-with-claude/usage-cost-api" rel="noopener noreferrer"&gt;Usage and Cost API&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenTelemetry:&lt;/strong&gt; set &lt;code&gt;CLAUDE_CODE_ENABLE_TELEMETRY=1&lt;/code&gt; to export &lt;code&gt;claude_code.cost.usage&lt;/code&gt; and &lt;code&gt;claude_code.token.usage&lt;/code&gt; to your own backend (&lt;a href="https://code.claude.com/docs/en/monitoring-usage" rel="noopener noreferrer"&gt;Monitoring&lt;/a&gt;). This is worth it if you already run Grafana or similar. It's overkill for one laptop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to reduce token usage
&lt;/h2&gt;

&lt;p&gt;Most token waste comes from context you no longer need. Anthropic lists these levers (&lt;a href="https://code.claude.com/docs/en/costs" rel="noopener noreferrer"&gt;Manage costs&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run &lt;code&gt;/clear&lt;/code&gt; between unrelated tasks.&lt;/strong&gt; It costs nothing. &lt;code&gt;/compact&lt;/code&gt; has to read the whole conversation to summarize it, so it is itself a large request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Sonnet by default.&lt;/strong&gt; Save Opus for hard reasoning. Set &lt;code&gt;model: haiku&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep CLAUDE.md under about 200 lines.&lt;/strong&gt; Move workflow-specific instructions into skills, which load only when needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer CLI tools such as &lt;code&gt;gh&lt;/code&gt; over MCP servers where possible.&lt;/strong&gt; Turn off unused servers in &lt;code&gt;/mcp&lt;/code&gt;, and run &lt;code&gt;/context&lt;/code&gt; to see what's taking up space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lower the effort level&lt;/strong&gt; with &lt;code&gt;/effort&lt;/code&gt; on simple jobs. Thinking tokens are billed as output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be careful with agent teams.&lt;/strong&gt; In plan mode they use about 7x the tokens of a standard session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch idle costs.&lt;/strong&gt; Scheduled &lt;code&gt;/loop&lt;/code&gt; tasks resend your full context on every run. Background work, such as summarizing past conversations for &lt;code&gt;--resume&lt;/code&gt;, typically costs under $0.04 per session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For context: across enterprise deployments, Anthropic reports an average of about $13 per developer per active day, and under $30 for 90% of users (&lt;a href="https://code.claude.com/docs/en/costs" rel="noopener noreferrer"&gt;Manage costs&lt;/a&gt;). Your numbers will differ, so measure your own first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;DIY today:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;/usage&lt;/code&gt; at the end of your next session and write down the total and the Prompt cache hit share.&lt;/li&gt;
&lt;li&gt;Put &lt;code&gt;--output-format json --max-budget-usd&lt;/code&gt; on one recurring &lt;code&gt;claude -p&lt;/code&gt; job and log &lt;code&gt;total_cost_usd&lt;/code&gt; to a CSV.&lt;/li&gt;
&lt;li&gt;If you use an API key, set &lt;strong&gt;Spend limits&lt;/strong&gt; on the Claude Code workspace in the Console.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;If you'd rather not manage per-token billing yourself:&lt;/strong&gt; 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 &lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;official pricing page&lt;/a&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Suits:&lt;/strong&gt; people who mainly work interactively and want a predictable monthly cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not for:&lt;/strong&gt; CI or &lt;code&gt;--bare&lt;/code&gt; jobs that need an API key, or anyone who needs exact per-run invoices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limitations:&lt;/strong&gt; plan usage resets on a rolling five-hour window and a weekly window, and going past the limit requires usage credits (&lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;Claude pricing&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Related guides on this site:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://en.aobaworks.com/claude-code-cron-job-setup-run-automations-overnight/" rel="noopener noreferrer"&gt;Claude Code Cron Job Setup: Run Automations Overnight&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.aobaworks.com/claude-code-hooks-lifecycle-every-event-explained/" rel="noopener noreferrer"&gt;Claude Code Hooks Lifecycle: Every Event, Explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.aobaworks.com/claude-code-checkpoints-how-rollback-works-in-automations/" rel="noopener noreferrer"&gt;Claude Code Checkpoints: How Rollback Works in Automations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does Claude Code still have a /cost command?
&lt;/h3&gt;

&lt;p&gt;Current documentation puts session token and cost figures under &lt;code&gt;/usage&lt;/code&gt;, though the changelog still mentions &lt;code&gt;/cost&lt;/code&gt; in recent entries. If &lt;code&gt;/cost&lt;/code&gt; behaves differently from an old guide, run &lt;code&gt;/usage&lt;/code&gt;. You'll find the same Session block there.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I see token usage for &lt;code&gt;claude -p&lt;/code&gt; runs?
&lt;/h3&gt;

&lt;p&gt;Add &lt;code&gt;--output-format json&lt;/code&gt; and read &lt;code&gt;total_cost_usd&lt;/code&gt; with &lt;code&gt;jq&lt;/code&gt;. For live events, use &lt;code&gt;--output-format stream-json --verbose&lt;/code&gt;; the last line is a &lt;code&gt;result&lt;/code&gt; message that includes the cost (&lt;a href="https://code.claude.com/docs/en/headless" rel="noopener noreferrer"&gt;headless docs&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Why doesn't the /usage cost match my bill?
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a hook stop Claude Code when it gets too expensive?
&lt;/h3&gt;

&lt;p&gt;Not directly, because &lt;code&gt;Stop&lt;/code&gt; and &lt;code&gt;SessionEnd&lt;/code&gt; hooks don't receive cost data. Use &lt;code&gt;--max-budget-usd&lt;/code&gt; for a hard cap on headless runs, and a Console spend limit for a cap Anthropic enforces.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Pro and Max users need to watch token costs?
&lt;/h3&gt;

&lt;p&gt;Not in dollars, since usage is included in the plan. Do watch the plan bars and the 24h/7d breakdown in &lt;code&gt;/usage&lt;/code&gt;, because long contexts and cache misses use up your limits faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/costs" rel="noopener noreferrer"&gt;Manage costs effectively – Claude Code Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/headless" rel="noopener noreferrer"&gt;Run Claude Code programmatically – Claude Code Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/cli-reference" rel="noopener noreferrer"&gt;CLI reference – Claude Code Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/commands" rel="noopener noreferrer"&gt;Commands – Claude Code Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference – Claude Code Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/statusline" rel="noopener noreferrer"&gt;Customize your status line – Claude Code Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/monitoring-usage" rel="noopener noreferrer"&gt;Monitoring usage (OpenTelemetry) – Claude Code Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/agent-sdk/cost-tracking" rel="noopener noreferrer"&gt;Track cost and usage – Agent SDK Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/changelog" rel="noopener noreferrer"&gt;Claude Code changelog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/docs/en/build-with-claude/usage-cost-api" rel="noopener noreferrer"&gt;Usage and Cost API – Claude Platform Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/docs/en/build-with-claude/workspaces" rel="noopener noreferrer"&gt;Workspaces – Claude Platform Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/usage" rel="noopener noreferrer"&gt;Claude Console Usage page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;Claude pricing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was created with the help of AI. Sources are linked inline; check the official documentation for the latest details.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>tokenusage</category>
      <category>costtracking</category>
      <category>headless</category>
    </item>
  </channel>
</rss>
