Skip to content

Hooks: Automatically Pulling the Trigger at Fixed Times

📚 Series Navigation: The previous article 32 Output Styles taught you how to change a "persona" to let Claude work with the tone you want. This article discusses another kind of "automation" — not changing how it talks, but running a sequence of actions automatically without fail the moment an event occurs: automatically formatting after modifying files, directly blocking dangerous commands, and sending you a notification after it finishes working. These are Hooks.

Imagine this number: In a certain week, the number of times you manually typed prettier --write after letting Claude modify the code was 23.

23 times. The same action, repeated mechanically 23 times. What's more outrageous is that you even missed it twice in between — it got bounced back by the CI formatting check after being submitted, and you had to run the process all over again.

At this point, you should think about one thing: For this kind of action that "must be done every time, and the content is exactly the same", why should it rely on people remembering it or Claude doing it consciously? You wrote "remember to run prettier after modifying files" in CLAUDE.md, but it always forgets once out of three times — because that is just a request, not a guarantee.

And by configuring a Hook, with one line of configuration, the problem is completely gone: From then on, every time Claude finishes editing a file, formatting runs automatically. You never type prettier manually again, and it never gets bounced back by CI again. This article will explain this thing that can "automatically pull the trigger", from what it is to how to configure and adjust it.

After reading this article, you will get:

  • Understand in one sentence what a Hook is and the fundamental difference between it and a "request written in CLAUDE.md"
  • What exact "timings" in Claude's working lifecycle can be hooked (PreToolUse / PostToolUse / Stop / SessionStart, etc.)
  • Which file the hook is configured in, and how matcher narrows it down to "trigger only when modifying files"
  • Three real examples you can copy directly: auto-format after editing, block dangerous commands, send a notification when done
  • What Hooks and Claude rely on to talk to each other (JSON from stdin, exit codes, stdout) — this is the key to understanding everything
  • How to troubleshoot step by step when a hook doesn't trigger or throws an error

01 First Understand: What Exactly is a Hook, and What "Guarantee" Does it Excel At

Conclusion first: A Hook is "a script or request that automatically executes the moment an event occurs" — it doesn't rely on Claude thinking to decide whether to do it, and its triggering is guaranteed. (The most commonly used are shell commands, but it also supports forms like HTTP endpoints, MCP tools, LLM prompts, etc.)

The official definition is very straightforward, here it is:

Hooks are user-defined shell commands executed at specific points in the Claude Code lifecycle. They provide deterministic control over Claude Code's behavior, ensuring certain actions always happen, rather than relying on the LLM to choose to run them.

Pay attention to two terms in there: "deterministic control" and "always happen". This is the core of Hooks.

Analogy: Automation rules posted at home ("When... then..."). You've probably set up those rules for smart homes — "When someone opens the door, then turn on the lights", "When I leave home, then turn off all sockets". Once the condition is met, the action inevitably happens, no need for anyone to remember. Hooks are this kind of rule installed for Claude Code — you define "When a certain event occurs, then run this command", and it executes automatically, without fail.

Here, a most critical distinction must be nailed down: What is written in CLAUDE.md is a "request" — it will probably comply, but might miss it; What is configured as a Hook is a "guarantee" — as long as that event triggers, the action is definitely executed, and it has nothing to do with whether Claude remembers. In official words:

Instructions like "never edit .env" in CLAUDE.md or skills are requests, not guarantees. A PreToolUse hook preventing editing is enforcement.

This is the root of the 23 times mentioned at the beginning: "run prettier after editing" written in CLAUDE.md is a request, missed once in three times; configured as a Hook, it is a guarantee, missed zero times.

Here are a few scenarios you'll likely encounter that deserve to be "put on guarantee", feel them out first:

  • "Automatically format / run lint every time after modifying files" — stop typing it manually, and don't expect it to be conscious
  • "Commands like rm -rf and deleting the production database, block them dead for me" — must be deterministically blocked, can't rely on prompts
  • "Send me a desktop notification when it finishes working or waits for my input" — so you can switch to do other things without staring at the terminal

💡 One-sentence summary: A Hook is an "automatic action triggered by an event". Its core value is to turn a "request" into a "guarantee" — what CLAUDE.md asks it to do might be missed, but once an event attached to a Hook triggers, it inevitably executes.


02 What "Timings" Can Be Hooked: Understanding Lifecycle Events

Hooks cannot be attached at just any time; they must be attached at specific "timings" in Claude's workflow. These timings are officially called events. To use Hooks well, the first step is to recognize "when do I want this to happen".

Recall the "agent loop" discussed in article 03 — Claude's workflow is a cycle of "Think → Act → Observe". These events happen to be scattered before and after this cycle. The official documentation categorizes them into three tiers based on trigger frequency. This categorization is very easy to remember:

  • Once per session: SessionStart (when session starts / resumes), SessionEnd (when session ends)
  • Once per turn: UserPromptSubmit (when you just submit a prompt, before Claude starts processing), Stop (when Claude finishes answering this turn)
  • Every tool call in the agent loop: PreToolUse (just before a tool executes), PostToolUse (after a tool successfully executes)

Just talking is a bit abstract, drawing a picture makes you understand at a glance where these most common events are stuck:

Claude Code Hooks 7 Timings: SessionStart → UserPromptSubmit → Pre/Post Tool Loop → Stop → SessionEnd

This picture unfolds the cycle of "Think → Act → Observe": entering the session is SessionStart, you speaking is UserPromptSubmit, and then entering the "whether to use a tool" loop — every time a tool is used, there is PreToolUse before it and PostToolUse after it. Once this round of the loop finishes, it is Stop, and closing up the whole session is SessionEnd. Wherever you want the action to happen, attach the corresponding event.

These six are the most used on a daily basis. In fact, there are about thirty events officially supported (such as PreCompact/PostCompact before and after compression, FileChanged for file writing changes, ConfigChange for configuration changes, SubagentStart/SubagentStop for starting and stopping subagents, etc.), but for beginners, thoroughly understanding the four below is enough to cover 90% of scenarios:

EventWhen it triggersMost typical usage
PreToolUseBefore a tool executesBlock dangerous commands, protect sensitive files (can prevent actions)
PostToolUseAfter a tool successfully executesAuto-format / run lint after modifying files
StopWhen Claude finishes answering this turnRemind "work isn't done yet, continue", scan the workspace
SessionStartWhen a session starts or resumesInject project state into the context (like recent commits)

There's a trick to remembering this table: look at the Pre and Post in the namesPre means "before", so only it can block before the action happens; Post means "after", the tool has already run, what's done is done, it can only "make a follow-up move" (format, log), but cannot block. This difference is detailed in the next section.

💡 One-sentence summary: Hooks are attached to specific events in Claude's lifecycle, divided into three tiers by frequency (per session / per turn / per tool call); beginners should first thoroughly understand PreToolUse (before, can block), PostToolUse (after, follow-up), Stop (finished answering), and SessionStart (opening).


03 Where Are Hooks Configured, and How Does matcher Narrow Them Down

Now that we know which events can be attached, let's look at how to write them. Hooks are written in the settings file (settings.json) — the configuration file specifically discussed in article 31. Which file it's written in determines its scope:

Which file to configureScopeCan it be shared with the team?
~/.claude/settings.jsonAll your projectsNo, only on your machine
.claude/settings.json (Project root)Only current projectYes, can be committed into git
.claude/settings.local.json (Project root)Only current projectNo, gitignored

This follows the same logic as "project filing cabinet vs. desk drawer" when discussing configuration in article 31: Hooks that the whole team should have (like "always format after editing") are written into the project's .claude/settings.json and committed to git; what only you want (like sending a notification to your desktop) is written into ~/.claude/settings.json.

What Does Hook Configuration Look Like

Let's look at a minimal complete example first — "Every time a file is modified using Edit or Write, automatically run prettier to format", which is the snippet that cures the 23-times habit. Write it into .claude/settings.json at the project root:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

Don't be intimidated by this nesting, it's just three layers. Break it down and you'll understand immediately:

  1. "PostToolUse" — Which event to attach to (here: after tool execution).
  2. "matcher": "Edit|Write"Narrow down to which tools trigger it (here: only after Edit or Write tools, not after Bash, Read).
  3. The inner hooks array — The action to actually run: "type": "command" means running a shell command, and "command" is that command.

The jq in this command is a small tool for parsing JSON (install with brew install jq on Mac, apt-get install jq on Ubuntu). Its role will be explained in the next section — simply put, it extracts the path of the file just modified from the data handed over by Claude and feeds it to prettier.

matcher: Making the Hook "Trigger Only When It Should"

matcher is the most crucial field to understand in a Hook configuration. In one sentence: Without it, the hook will trigger on "every single time" that event occurs; with it, you can narrow the scope.

Analogy: The security guard's entry rules — if they aren't delivering goods, don't let them in. A hook without a matcher is like telling a security guard to "register anyone who comes", which is highly inefficient; with a matcher, it becomes "only register when a courier comes, let others through directly". matcher does the job of "defining the trigger scope".

For tool-type events (PreToolUse/PostToolUse), matcher matches the tool name. There are three ways to write it, look at this table:

Your matcherMeaningExample
"Edit|Write"Exact match for these tools (| is "or")Triggers only after Edit or Write
"Bash"Exact match for a single toolTriggers only when running Bash command
"" or omittedMatches all, triggers every timeRuns every time this event occurs

Note: matcher is case-sensitive. Writing edit will not match the Edit tool — this is one of the most common reasons why beginner hooks don't trigger.

There is another point beginners easily overlook: Some events do not support matcher at all (like UserPromptSubmit, Stop), because they don't have something like a "tool name" to filter by, and always trigger every time. Adding a matcher to these events will be silently ignored.

💡 One-sentence summary: Hooks are written into settings.json (global in home directory, project in .claude/). The configuration has three layers — event, matcher, action; matcher is responsible for narrowing the hook down to "trigger only on the tools it should", and is case-sensitive.


04 How Hooks and Claude Talk to Each Other: stdin, Exit Codes, stdout

This section is the key to understanding everything. Why can the jq command from before get the file path? How does a hook "block" a command? The answers are all in this "dialogue mechanism".

The mechanism itself is extremely simple, just three pipelines: Claude feeds event data from stdin to your script → the script works → the script tells Claude what to do next using "exit code + stdout". Let's break them down one by one.

Input: Claude Hands You a Chunk of JSON from stdin

Once an event triggers, Claude Code will stuff the relevant data for this event as a piece of JSON from standard input (stdin) to your command. For example, when Claude is about to run a Bash command, the PreToolUse hook receives something like this:

json
{
  "session_id": "abc123",
  "cwd": "/Users/sarah/myproject",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test"
  }
}

See? What Claude wants to do, which tool it's using, and what the parameters are, are all inside. The command jq -r '.tool_input.file_path' in section 03 does exactly this: extracting tool_input.file_path (the file path to be edited) from this chunk of JSON. jq is a tool specifically for parsing JSON, and -r tells it to output raw text (without quotes).

Output: Using "Exit Code" to Tell Claude the Next Step

After the script finishes its work, it gives commands to Claude via the exit code. This is the core convention of Hooks. Just remember three numbers:

Exit CodeMeaningEffect
0No objections, proceed normallyOperation continues (During PreToolUse, this does not equal approval, it goes through the normal permission process)
2Block it!Operation is blocked; what you write to stderr will be passed as feedback to Claude, letting it adjust
Others (e.g., 1)Error occurred, but don't blockOperation continues, terminal displays a hook error prompt

The key is exit 2 — this is the only way for a hook to "hit the brakes". Note a counter-intuitive trap:

For most hook events, only exit code 2 prevents the action. Claude Code treats exit code 1 as a non-blocking error and proceeds, even though 1 is the traditional Unix failure code. If your hook is meant to enforce a policy, use exit 2.

Translated into human language: If you want to block an operation, you must exit 2, not exit 1. Many people write exit 1 out of Unix habit, and the result is that the hook "reported an error but didn't block it", and the command ran anyway. This is the easiest trap to fall into when writing a blocking hook for the first time — the script clearly detected the dangerous command and printed a warning, but because they conveniently wrote exit 1, Claude ran it anyway, giving you a cold sweat.

There is another detail related to "whether it can be blocked": Only Pre events can truly block an operation. PostToolUse receiving exit 2 won't block it either — because the tool has already run, the wood is already a boat, and it can only show the stderr to Claude. This echoes the sentence in the previous section " Pre can block, Post can only follow up".

Advanced: Using stdout to Return JSON for Finer Control

Exit codes can only toggle "block / don't block". If you want finer control (like blocking while telling Claude the specific reason, or injecting a piece of info into its context), change it to exit 0 and print a piece of JSON to stdout.

Use exit code 2 paired with stderr to "prevent", or use JSON paired with exit code 0 for "structured control". Do not mix the two: when you exit with 2, Claude Code will ignore the JSON.

Here are the two most common JSON outputs:

PreToolUse wants to block and explain the reason — use permissionDecision:

json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "This command modifies the production db, execution is prohibited."
  }
}

permissionDecision has four values: "deny" (block, send the reason to Claude), "ask" (pop up the permission box normally to ask you), "allow" (skip the permission box and allow directly), "defer" (delay execution, let the tool resume later, suitable for asynchronous approval scenarios in non-interactive mode).

Here is a security red line that must be clarified, echoing permissions and security in articles 20 and 21: A hook returning "allow" cannot bypass the deny rules in your settings. Official words —

Returning "allow" skips the interactive prompt but does not override permission rules. If a deny rule matches the tool call, the call will be blocked even if your hook returns "allow".

In other words: Hooks can only "tighten" restrictions, not "loosen" them beyond what the permission rules allow. This is a very important security design — it ensures that malicious hooks cannot tear down your safety guardrails by returning allow. Conversely, the block priority of a PreToolUse hook is extremely high: even if you turn on --dangerously-skip-permissions (skipping all permissions), a hook returning deny can still block it dead. So using Hooks to enforce team red lines is truly a dead block.

SessionStart wants to stuff some info into the context — just print text directly to stdout (these events are special, stdout will be treated as context and fed to Claude). For example, telling it the recent 5 commits as soon as the session opens:

json
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "git log --oneline -5"
          }
        ]
      }
    ]
  }
}

💡 One-sentence summary: Hooks and Claude converse via three pipelines — stdin feeds JSON in, exit codes issue instructions, stdout does fine control; memorize that "exit 2 is the only way to block (not 1)", "don't mix exit codes and JSON", and "Hooks can only tighten, not loosen permissions", and you've grasped 70%.


05 Three Real Examples You Can Copy Directly

Enough theory, here are three common hooks you can copy directly. Each one clearly marks "which event to attach to, what to narrow down to, which file to configure in".

Example 1: Automatically format after modifying files (PostToolUse)

This is the one that cures the 23-times habit, most practical, zero risk, strongly recommended for every project. Write into .claude/settings.json at the project root:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

Logic: Every time Claude finishes modifying a file using Edit/Write → the hook extracts the file path from the stdin JSON → tosses it to prettier --write to format. From then on, formatting is always unified, you don't need to worry about it. Swapping prettier for eslint --fix, gofmt, or black is the same routine.

Example 2: Block dangerous commands (PreToolUse + script)

This uses "exit 2 blocking". When commands are complex, writing the logic into a separate script is much cleaner than hardcoding it into JSON.

Step 1, save the script to .claude/hooks/block-dangerous.sh:

bash
#!/bin/bash
# block-dangerous.sh: block dangerous commands like rm -rf
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')

if echo "$COMMAND" | grep -q "rm -rf"; then
  echo "Blocked: Detected rm -rf, intercepted" >&2   # Write to stderr, will be fed back to Claude
  exit 2                                             # exit 2 = prevent this tool call
fi

exit 0   # Let other commands through, follow normal permission flow

Step 2, add executable permissions to the script (required on Mac/Linux, otherwise Claude can't run it):

bash
chmod +x .claude/hooks/block-dangerous.sh

Step 3, register it in .claude/settings.json, attach it to the PreToolUse of the Bash tool:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous.sh"
          }
        ]
      }
    ]
  }
}

The $CLAUDE_PROJECT_DIR here is an environment variable provided by Claude Code, pointing to the project root directory — using it to piece together the path ensures the hook can find the script no matter which subdirectory it runs in, which is more stable than a hardcoded relative path.

⚠️ This section loops back to the main security thread of article 21: Hooks are shells run with your full user permissions, and can delete any file you can delete. Officially emphasized repeatedly, before adding any hook, review its command first, especially don't just copy whole chunks of scripts from unknown sources and stuff them into settings.

Example 3: Send a desktop notification when it needs your input (Notification)

When Claude is halfway done and needs your approval, or finishes answering and waits for your next sentence, you might have long switched to browsing something else. Attach a notification hook, and it will actively call you. This uses the Notification event (triggered when Claude sends a notification).

macOS written into ~/.claude/settings.json (this kind of "call me" hook is your personal preference, put it globally):

json
{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code is waiting for you\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

Platform differences (native commands provided officially, just copy them):

PlatformNotification Command (fill into command)
macOSosascript -e 'display notification "..." with title "Claude Code"'
Linuxnotify-send 'Claude Code' '...'
WindowsUse PowerShell's MessageBox (official docs have full snippets)

On macOS, if the notification doesn't pop up, it's mostly because Script Editor hasn't gotten notification permissions — go to "System Settings → Notifications", find Script Editor, and turn on the switch. It's easy for it to be completely silent the first time you configure it, and after fussing around for a long time, you realize this permission wasn't given.

💡 One-sentence summary: Three hooks with increasing risk — formatting (PostToolUse, zero risk, recommended for everyone), blocking commands (PreToolUse + script, remember chmod +x and exit 2), sending notifications (Notification, different platform commands); using $CLAUDE_PROJECT_DIR for script paths is the most stable.


06 Hands-on: Configure a Hook in 5 Minutes and See It Trigger

Just reading without practicing won't make you remember. Below I'll take you through configuring a safest, easiest-to-see-effects hook — every time Claude finishes running a Bash command, log this command into a log file. The whole process won't touch any of your code, just adds a piece of configuration, and you can verify it with your own eyes after running.

This practice uses jq. If you haven't installed it: run brew install jq on Mac, sudo apt-get install jq on Ubuntu. You don't need a VPN to install it.

Step 1: Find a practice directory, open its project settings file

Just find an empty directory (don't practice in an important project), and create .claude/settings.json inside it. If the file already exists and has other content, add the hooks part as a new key, don't overwrite the whole thing. File content:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' >> ~/claude-bash-log.txt"
          }
        ]
      }
    ]
  }
}

This hook: Every time a Bash tool finishes running (PostToolUse + matcher: "Bash") → extracts the command from the stdin JSON → appends it to claude-bash-log.txt in the home directory using >>.

Step 2: Start Claude in this directory, confirm the hook is registered

bash
claude

After entering, type /hooks:

text
/hooks

Expected: A read-only hook browser pops up, listing all events. Find PostToolUse, it should show 1 hook next to it. Selecting it allows you to see the details: event, matcher (Bash), source file (Project, i.e., the project's .claude/settings.json), and that command. Seeing it in the list = hook registered successfully.

The /hooks menu is read-only — it only lets you look, not add or modify. To change hooks, edit settings.json directly, or let Claude change it for you directly.

Step 3: Let Claude run a Bash command to trigger the hook

Press Esc to return to the chat, let it run a harmless command:

text
Help me use ls to see what files are in the current directory

It will call the Bash tool to run ls. With this run, the PostToolUse hook should trigger. (When a hook executes successfully, it is "silent", the terminal won't specifically prompt you — this is normal.)

Step 4: Verify the hook really ran — check the log file

Open a new terminal, look at the log:

bash
cat ~/claude-bash-log.txt

Expected: That ls command (and other Bash commands Claude ran in this session) appears in the file. Seeing the command recorded = the hook really triggered automatically after every Bash, your "automation rule" is established.

Step 5: Cleanup (Optional)

It's very simple to dismantle after practice — just delete that hooks section in .claude/settings.json (hooks don't have a separate "delete command", removing the entry from the config is enough). Delete the log file rm ~/claude-bash-log.txt.

By running through these five steps, you've personally walked through the complete chain of "writing config → /hooks confirming registration → triggering event → verifying side effects". In the future, configuring any hook is essentially this process, just changing the event, changing the matcher, changing the command.

💡 One-sentence summary: For practice, configuring a zero-risk hook like "logging Bash commands" is the steadiest — write into .claude/settings.json, use /hooks to see it registered, let Claude run a command to trigger it, check the log file to verify; seeing the side effect happen with your own eyes is more useful than memorizing ten definitions.


07 The Hook Doesn't Trigger / Throws an Error, How to Troubleshoot

A configured Hook not working is the most common sticking point for beginners. Don't guess blindly, check in the following order, and you can basically pinpoint it. I've organized the official troubleshooting checklist into "Symptom → How to check":

SymptomMost Likely Cause / How to Check
Hook doesn't trigger at all① Run /hooks to see if it's even registered; ② matcher is case-sensitive, edit doesn't match Edit; ③ Event attached incorrectly (to block operations use PreToolUse, not PostToolUse)
My configured hook is nowhere in /hooks① JSON format is wrong (JSON doesn't allow trailing commas, doesn't allow comments); ② File location is wrong (project hooks in .claude/settings.json, global in ~/.claude/settings.json); ③ Restart the session if changes haven't taken effect
Terminal reports hook errorScript exited with an unexpected non-zero status. Test it manually (see command below); reporting command not found is mostly because the script path is wrong, use absolute paths or $CLAUDE_PROJECT_DIR; reporting jq: command not found means jq isn't installed
Script didn't runForgot to add executable permissions to the script on Mac/Linux, add chmod +x
Wanted to block but didn'tEight times out of ten it's writing exit 1, change it to exit 2 (see the trap in section 04)

Two most practical troubleshooting methods singled out:

① Feed fake data manually to test the script. You don't have to trigger it for real in Claude, make a piece of JSON yourself and pipe it to the script to see if the exit code is right:

bash
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/x"}}' | ./block-dangerous.sh
echo $?   # Check exit code: blocking script should output 2 here

This is the standard action for debugging a blocking hook — first get the script fully working independently, then attach it to Claude, saving you from testing repeatedly in a real session.

② Turn on debug logs to see details. To see "which hooks exactly matched, what the exit code was, what stdout/stderr were", start with --debug, the logs will be written into ~/.claude/debug/<session_id>.txt:

bash
claude --debug

Or just type /debug directly in the session to turn it on. There will be lines like this in the log, letting you see at a glance whether the hook ran, and how it ran:

text
[DEBUG] Executing hooks for PostToolUse:Bash
[DEBUG] Hook command completed with status 0

There's also a one-click switch worth knowing: If you want to temporarily turn off all hooks (e.g., suspecting a hook is causing trouble), just add "disableAllHooks": true to the settings file, no need to delete them one by one.

💡 One-sentence summary: If a hook doesn't work, don't guess blindly, check in the order of "/hooks to see registration → check matcher case and event selection → manually feed JSON to test script → --debug to see logs"; if it didn't block when you wanted it to, first see if you wrote exit 1.


08 Summary

In this article, we took Hooks all the way from "what they are" to "how to configure and adjust them" — They are "automation rules" installed for Claude Code: the moment a certain event triggers, it runs a sequence of actions for you without fail.

Stringing the core points together for review:

What you want to doHow to implementKey points
Understand what a Hook isEvent-triggered automatic shell commandsTurn "requests" into "guarantees", don't rely on Claude's consciousness
Choose right attachment timingRecognize lifecycle eventsPre can block, Post follows up, Stop finishes answering, SessionStart opens
Write a hookConfigure in settings.jsonThree layers: event + matcher (case-sensitive) + action
Let the hook talk to Claudestdin / exit code / stdoutOnly exit 2 can block, don't mix exit codes and JSON
Block dangerous operationsPreToolUse + scriptRemember chmod +x, Hooks can only tighten, not loosen permissions
Hook is not workingTroubleshoot in order/hooks to see registration, manually feed JSON to test, --debug to see logs

You should now be able to: Explain the fundamental difference between a Hook and a "request written in CLAUDE.md" (guarantee vs request); know which step in Claude's workflow PreToolUse/PostToolUse/Stop/SessionStart each attaches to; write a hook with a matcher into settings.json following the template; understand that hooks talk to Claude relying on stdin/exit code/stdout, and memorize "only exit 2 can block"; know to start investigating from /hooks and --debug when a hook doesn't trigger. The drudgery of manually typing prettier 23 times at the beginning, by now you have the ability to solve it permanently with one line of configuration.

Hooks are a very hardcore piece in this group of "System Configuration and Optimization" — they give you deterministic control over Claude's behavior, not just "asking it nicely".


Next article 34 "CLI Reference Manual: Commands and All Flags" — Along the way, you've typed quite a few commands starting with claude, used flags like --debug, --dangerously-skip-permissions, but these are actually just the tip of the iceberg. The next article systematically combs through all commands and flags of the claude command line, treating it as a "dictionary" you can flip through anytime. Think about it: how many claude flags can you blurt out right now? After flipping through that article, you'll find you've previously missed out on at least half of the switches that can save you trouble.