NeuralOS
GuideAdvanced

Claude Code hooks · automate your workflow with the agent's lifecycle events

You already know how to write a good contract for your agent: you tell it in the AGENTS.md how you want it to work. The problem is that an LLM reads that contract and decides all over again on every turn — so the critical rule gets followed almost every time, and that "almost" is exactly the one that bites you. Hooks are the next rung: you take the rule out of the model's head and put it into the infrastructure, so it happens 100% of the time without depending on its judgment. I'll walk you through the lifecycle events where you hook in, the anatomy of the JSON, where each hook lives and why that matters, and I'll leave you three real hooks ready to paste (auto-format, blocking dangerous commands, desktop notification) drawn from Anthropic's official docs. A single master prompt to set up your first one with safety built in — because a hook runs shell with your permissions. It's the leap from "write the contract" to "make the contract run itself".

Jul 19, 202614 min
Who is this for?
For you — someone who already builds with AI in Claude Code and has spent weeks repeating the same phrase: "remember to format", "don't touch the .env", "run the tests before committing". You forget, the AI forgets, and one day a change slips through that shouldn't have. Hooks are the leap from "I ask it to remember" to "it happens every time, even when nobody remembers". You don't need to be a programmer: you need to understand one pattern and copy three JSON blocks.

1. The moment · when you realize "asking the AI" isn't enough

There comes a point in every AI project where you stop trusting the model's good intentions. You wrote in your AGENTS.md that it should format the code, not touch the sensitive files, run the tests. And 90% of the time it does. But the remaining 10% is exactly the one that bites you: the time it edited the production .env, the time it skipped formatting and left the commit messy, the time it ran an rm -rf on the wrong folder because "it looked temporary".

The hook moment is when you think: "what if this didn't depend on the model deciding to do it right? What if it just happened, every time, without relying on its judgment?". That's exactly the promise the official Claude Code docs put at the center of the hooks page: they give "deterministic control" over the agent's behavior, "ensuring certain actions always happen rather than relying on the LLM to choose to run them" (faithful translation of the English original). That sentence is the whole thesis of this resource.

Picture it like this
A contract (your AGENTS.md) is like the rulebook pinned to a factory wall: it says how things should be done, and you trust every worker to read and follow it. A hook is the sensor on the conveyor belt that physically stops the machine if a part comes out wrong. The rulebook persuades; the sensor enforces. Hooks are the sensor — they don't say please, they cut the power.

2. The pain · where it comes from and why it happens

The pain isn't an apocalypse, it's erosion. You asked the AI to do something "every time" and sometimes it doesn't, because an LLM isn't deterministic: each turn it decides all over again, and in a turn loaded with context, your reminder to format can get buried under a thousand tokens of something else. It's not that the AI is disobedient — it's that you're asking for consistency from a probabilistic system. It's like asking someone brilliant but scatterbrained to never forget to turn off the gas: most nights they turn it off, and that's exactly why you let your guard down.

Symptoms that you need hooks
You repeat the same instruction every session ("format", "don't touch X", "run the tests") and it still skips it sometimes.
You found out afterward that the AI edited a file it should never touch: .env, a package-lock.json, something inside .git/.
A commit slipped through with inconsistent formatting and messed up the diff for the whole team.
You're a bit scared to leave the AI with broad permissions because there's no safety net that doesn't depend on the AI itself.
Your AGENTS.md has critical rules written in prose, and "written" isn't the same as "guaranteed".
The most common misunderstanding
A lot of people think that if they write the rule in ALL CAPS with exclamation marks in the AGENTS.md, it's now "enforced". It isn't. No matter how emphatic it is, it's still text the model reads and decides whether or not to obey. The only way to make something truly happen —100% of the time, no exceptions— is to take it out of the LLM's head and put it into the infrastructure. That's hooks.

And here's the angle that makes this an advanced resource: if you've already read about writing a good contract for your agent (the AGENTS.md), hooks are the next rung. You go from "write the contract" (declarative: you tell it what you want) to "make the contract run itself" (deterministic: the machine enforces it for you). It's the difference between a law written on paper and a cop standing on the corner.

3. What a hook is (no fluff, in one sentence)

A hook is a shell command that Claude Code runs automatically at a specific point in its lifecycle. You declare it once in a settings.json file; from then on, every time that point in the cycle happens, your command runs. It's not triggered by the model's judgment: it's triggered by the event. That's the magic — it's deterministic, not up for debate.

The underlying idea
Claude Code emits signals as it works: "I just received a prompt", "I'm about to use a tool", "I finished editing a file", "I'm going to compact the context", "I finished responding". A hook is hooking into one of those signals and saying: "when that happens, run THIS". To hook — hence the name and the emoji 🪝.

4. The lifecycle events (the points where you hook in)

The official docs list many lifecycle events — there's one for almost any moment in the agent's work. Don't memorize the count or the full list: learn the idea and keep the ones you'll actually use 95% of the time. Here they are, with their real names exactly as they appear in the docs:

The events that matter (real names from the official docs)
`SessionStart` — when a session starts or resumes. Ideal for injecting fresh context (e.g. your conventions, the latest commits). Its stdout output gets added to Claude's context.
`UserPromptSubmit` — right when you send a prompt, before Claude processes it. You can add context or even block the prompt.
`PreToolUse`before a tool runs. This one can block the action. It's your bodyguard: this is where you stop an rm -rf or an edit to a protected file.
`PostToolUse`after a tool succeeds. Here you format the file just edited, run a lint, whatever you want.
`Notification` — when Claude needs you (asks for permission or is waiting). Perfect for a desktop notification.
`Stop` / `SubagentStop` — when Claude (or a subagent) finishes responding. Useful for a final check before calling the turn done.
`PreCompact` (and SessionStart with the compact matcher) — around context compaction, so you don't lose what matters when memory gets compressed.
`SessionEnd` — when the session ends. To clean up temp files, close things out.
The distinction that clears everything up · Pre vs. Post
`PreToolUse` happens BEFORE and CAN block (the tool hasn't run yet, so you can veto it). `PostToolUse` happens AFTER and CANNOT undo (the tool already ran; the docs say so plainly). Mental rule: if you want to prevent something, hook into Pre. If you want to react to something that already happened (format, notify, log), hook into Post.

5. How a hook is declared (the anatomy of the JSON)

Every hook lives inside a "hooks" block in your settings.json. The structure is always the same nesting doll: the event name → a matcher (what it applies to) → a list of hooks with type: "command" and the command to run. Look at it once and you'll never forget it:

json
{
  "hooks": {
    "PostToolUse": [                    // 1. the lifecycle EVENT
      {
        "matcher": "Edit|Write",         // 2. WHICH tools it applies to
        "hooks": [
          {
            "type": "command",           // 3. it's a shell command
            "command": "...your command..." // 4. WHAT to run when it happens
          }
        ]
      }
    ]
  }
}
The matcher, in plain terms
The matcher filters what the hook applies to. For tool events (PreToolUse, PostToolUse) it filters by tool name: "Edit|Write" means "only when it uses Edit or Write", "Bash" only shell commands. An empty matcher ("") fires every time, for everything. The pipe | separates alternatives (in recent Claude Code versions a comma works too: "Edit, Write" is equivalent).

6. Where a hook lives (and why the place matters)

Where you put the hook defines its scope. This is key: a project hook is shared with your team (it's committed to the repo); a global one is just yours. The official table sums up where each thing goes:

The three places a hook can live
`~/.claude/settings.json` — global, applies to all your projects. Not shared, it's your machine's. Your personal hooks go here (desktop notifications, for example).
`.claude/settings.json` (inside the repo) — that project only, and can be committed: your whole team inherits the rule. Project rules go here (formatting, protected files).
`.claude/settings.local.json` — that project only, private: Claude Code gitignores it when it creates it. For your local settings you don't want to share.
The golden rule of place
If the rule must be followed by everyone who works in this repo (format, don't touch sensitive files), it goes in .claude/settings.json and gets committed. That way the contract stops living in one person's head and starts traveling with the code. That's the real power: discipline becomes part of the repository, not a sticky note someone has to remember.

7. Three copy-paste hooks worth their weight in gold

Enough theory. Here are three real hooks, drawn from the official docs, ready to paste. Start with the one that hurts most today.

a) Auto-format after every edit (PostToolUse)

The classic. Every time Claude edits or writes a file, Prettier runs automatically. Never again a commit with inconsistent formatting. Goes in your project's .claude/settings.json:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}
How it works under the hood (so it's not magic)
When the event fires, Claude Code passes your command a JSON over standard input (stdin) with the data: which tool it used, on which file, etc. Here jq -r '.tool_input.file_path' extracts the edited file's path from that JSON, and xargs npx prettier --write hands it to Prettier. jq is a command-line JSON reader — install it with brew install jq on macOS or apt-get install jq on Debian/Ubuntu.

b) Block dangerous commands like rm -rf (PreToolUse)

This is the one that takes the fear away. A PreToolUse hook that reviews every Bash command before running it and vetoes it if it contains anything destructive. Here a technical detail the docs make crystal clear does matter: it blocks by exiting with code 2, and whatever you write to stderr reaches Claude as an explanation so it can correct itself. First the script:

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

if echo "$COMMAND" | grep -q "rm -rf"; then
  echo "Blocked: 'rm -rf' is not allowed. Delete specific paths, not recursive-force." >&2
  exit 2   # exit 2 = blocks the action; stderr reaches Claude as feedback
fi

exit 0     # exit 0 = no objection; normal permission flow continues

Make it executable and register it in your settings pointing to the script:

bash
chmod +x .claude/hooks/block-rm-rf.sh
json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh"
          }
        ]
      }
    ]
  }
}
Honesty · this grep is a demo, not an airtight lock
A grep -q "rm -rf" catches the obvious case, but it's slipped past by an rm -fr, an rm -rf with weird spacing, or an rm --recursive --force. It works as a visible net and educational feedback for the AI, but don't mistake it for armor. The docs themselves say it plainly: hook filtering is best-effort and "fails open" (if it can't parse the command, it lets it through), so for a HARD ban nobody can skip, use the permission system (permissions.deny), not a hook. The correct pattern is: hook to react and warn; permission rules for the real lock.
The detail that truly saves you · the hook wins even in bypass mode
This is the most powerful part and almost nobody knows it: according to the official docs, a PreToolUse that returns a block decision vetoes the tool EVEN in `bypassPermissions` or with `--dangerously-skip-permissions`. That is: even if you (or the AI) have dropped every permission barrier, your blocking hook still stands. The rule is asymmetric and designed that way on purpose: hooks can tighten the policy, never loosen it beyond what the permission rules allow. A blocking hook is a rule not even you can skip by accident.

c) Desktop notification when the AI needs you (Notification)

To stop babysitting the terminal. When Claude finishes or needs your permission, a desktop notification pops up and you go do something else. Goes in your global ~/.claude/settings.json (macOS example with osascript):

json
{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}
On Linux and Windows
The same hook, changing the command: on Linux use notify-send 'Claude Code' 'Claude Code needs your attention'; on Windows (PowerShell) a MessageBox from System.Windows.Forms. The JSON structure is identical — only the command line changes. A macOS trick the guide itself documents: if the notification doesn't show up, run osascript -e 'display notification "test"' once and then grant Script Editor permission in System Settings → Notifications.

8. The habit · when to think of a hook (and when NOT to)

Hooks aren't for everything. The mental rule is simple: if something must always happen, mechanically and without judgment, it's a hook. If something requires case-by-case judgment, don't force it with a deterministic hook — let the model decide (or use the prompt/agent-type hooks the docs also support, but that's another league).

Moments when you should ALWAYS think of a hook
Every time you catch yourself writing "remember to…" for the third time in your AGENTS.md. If you're repeating it, turn it into a hook.
When there's an action that should never happen (touch .env, delete recursively, write to .git/): that's a blocking PreToolUse — backed by a permission rule for the hard lock.
When there's an action that should always happen after editing (format, sort imports, run a lint): that's a PostToolUse.
When you want a critical context re-injected after every compaction, so the AI doesn't "forget" your conventions mid-session.
The honest limit of hooks
A command hook runs deterministic shell: it works for mechanical rules (this path yes, this one no), not for nuanced judgments ("is this change a good idea?"). And watch out with PostToolUse: it can't undo what already happened — it only reacts. To prevent, always PreToolUse. Cramming too much complex logic into a hook makes it fragile; keep them short and single-purpose.
Security · hooks run shell with YOUR permissions
The official docs warn about it plainly and it bears repeating: a hook runs arbitrary shell commands with your credentials, automatically. Never paste a hook you don't understand, never copy one from a source you don't trust, and review every command before saving it. A malicious hook is a malicious command running on its own. Read them the way you'd read any script you're about to run on your machine.

9. The usage protocol · set up your first useful hook

Here's the practical detail: the docs themselves recommend asking Claude to write the hook by describing it. But for it to come out right —and be safe— it's worth giving it a structured request, not a "give me a hook". This is THE master prompt of the resource: a single, powerful one that designs your first hook with the safety brakes on.

Master prompt · design your first useful hook (with safety)text
I want to set up my first Claude Code hook to automate a rule in my workflow. The rule I want to guarantee ALWAYS happens is: [DESCRIBE THE RULE, e.g.: "format every file you edit with prettier" / "block any command that deletes files recursively" / "prevent editing the .env or anything inside .git/" / "notify me when you finish"].

Before writing anything, help me design it well by answering this in plain language (I don't code):

1. THE CORRECT EVENT in the lifecycle for this rule. If the rule is to PREVENT something, it must be PreToolUse (happens before and can block). If it's to REACT to something already done (format, notify, log), it must be PostToolUse or Notification. Tell me which you pick and why.

2. THE EXACT MATCHER (which tools it applies to: Edit|Write, Bash, or empty for all) and why that one and not another. Keep it as NARROW as possible: a matcher that's too broad fires the hook where it shouldn't.

3. THE PLACE where the settings.json should live:
   - If the rule must be followed by the whole team on this repo → .claude/settings.json (gets committed).
   - If it's just for my machine and all my projects → ~/.claude/settings.json.
   - If it's local and private → .claude/settings.local.json.
   Tell me which and why.

4. THE FULL JSON BLOCK, ready to paste, with the hook already set up. If it needs a separate script (typical for blocks with PreToolUse), give me the full .sh script too and remind me to make it executable with chmod +x.

5. IF IT'S A BLOCKING HOOK: use it with exit 2 and a clear message to stderr explaining to me why it was blocked, so you yourself (Claude) receive the reason and correct yourself. Don't make it silent. And WARN ME if this rule also deserves a permission rule (permissions.deny) as a hard lock, because a grep in a hook is best-effort and can be skipped.

6. SECURITY, mandatory: explain to me in one sentence WHAT shell command this hook will run and confirm it does nothing destructive and sends no data anywhere. Remind me that a hook runs shell with my permissions and that I should only save commands I understand.

7. HOW I TEST IT: tell me how to verify the hook is registered (the /hooks command inside Claude Code) and a way to test that it actually fires, without breaking anything real.

First show me ONLY the design (event, matcher, place), then the JSON and the script if needed, and at the end the security explanation and the test. Don't modify my settings.json yet: I want to review the block before I paste it myself.

Notice the pattern: first you design, you review the security, and you paste the block. You never let a hook appear in your config without having understood it. It's the same two-step discipline that protects any powerful automation: the machine proposes, you approve.

10. The easiest paths · what you do by chat and what by file

So you don't get tangled up, here's what you can delegate to the AI over chat and what stays your job:

What the AI does for you (over chat)
Write the full hook JSON block if you describe the rule to it (the official docs recommend this explicitly).
Generate the .sh script for blocking hooks, with its jq, its exit 2, and its stderr message.
Explain which event and which matcher fit your rule, and which settings.json it should live in.
Remind you of the install steps: chmod +x the script, brew install jq if you're missing it.
What YOU decide and do (don't delegate it)
Paste the block into your settings.json — after reading and understanding it. The safety net can't be code you didn't review.
Choose the place (shared repo vs. global vs. local): that defines who inherits the rule.
Confirm the hook's command is safe (it runs shell with your permissions: if you don't understand it, don't save it).
Verify with `/hooks` inside Claude Code that it got registered, and test that it fires without breaking anything real.
The trick for verifying
Inside Claude Code, type `/hooks` to open the hooks browser: you'll see all the configured ones grouped by event, with their matcher and command. It's read-only (to edit, you touch the JSON or ask the AI), but it's your confirmation that the hook exists and is where it should be. If it doesn't show up, check that the JSON is valid (no trailing commas or comments) and that the file is in the right path; sometimes restarting the session is enough for the file watcher to pick it up.

Summary · your hooks checklist

Before calling a hook done, confirm
I picked the correct event: PreToolUse to prevent, PostToolUse/Notification to react.
The matcher is as narrow as possible (it doesn't fire where it shouldn't).
It's in the right place: shared repo, your global, or private local.
If it blocks, it uses `exit 2` with a clear message to stderr so the AI corrects itself — and if it's a hard ban, I backed it with a permission rule.
I understand the command it runs (shell with my permissions): I saved nothing I didn't review.
I verified it with `/hooks` and tested that it fires without breaking anything real.
I took the rule out of my AGENTS.md prose and turned it into something that happens every time.
In NeuralOS · the same discipline, at platform scale
Hooks embody an idea we take seriously in NeuralOS: the layer where critical rules don't depend on anyone's judgment because the infrastructure itself enforces them. It's exactly what the platform's Supabase advisors gate does under the hood: before sealing any database change, an automated check runs on its own and blocks the merge if something violates the security rules. Nobody has to "remember" to review it — it happens every time, like a PreToolUse that vetoes what shouldn't get in. It's the same philosophy as a hook, applied to a whole team instead of a single terminal: making quality not a promise someone remembers, but a sensor on the belt that can't be skipped. You set the rule once; the system enforces it forever.
The C-A-R protocol · build without bugs
Hooks are the deterministic arm of C-A-R: where the protocol calls for discipline, a hook makes it automatic. The perfect pair for making quality not depend on remembering.
Loop engineering · the AI works on its own toward the goal
When you let the AI iterate on its own in a loop, hooks are its brakes and guardrails: they guarantee that certain things happen (or DON'T happen) on every pass, without relying on its judgment.
Official docs · Automate actions with hooks (Anthropic)
The source of everything in this resource: the lifecycle events, the copy-paste examples, and the security considerations. Read it before deploying hooks in a shared environment.
#hooks#Claude Code#automation#lifecycle#advanced#determinism
Ready to build?

Start building in
under 3 minutes

Join 4,200+ builders. No credit card. Build your first app with AI in minutes.