BROKE → BUILT EST. 2026 · BUILDING IN PUBLIC · $0 → $4.99
Guides Jul 2, 2026

How to Use Claude Code Hooks (Turn Rules Into Guarantees)

Claude Code hooks run shell commands at lifecycle events — auto-format after edits, block protected files, audit every command. Config, exit codes, recipes.

BROKE → BUILT · GUIDE How to Use Claude CodeHooks (Turn Rules IntoGuarantees) broke2builtai.com
Short answer

Claude Code hooks are user-defined shell commands that run automatically at lifecycle events like PreToolUse, PostToolUse, UserPromptSubmit, and Stop. Configure them under a "hooks" key in ~/.claude/settings.json or your project's .claude/settings.json — or run the /hooks slash command to set them up interactively. Each hook receives the event's data as JSON on stdin; exit code 0 continues normally, while exit 2 blocks the action and feeds your stderr message back to Claude so it can adjust. Unlike CLAUDE.md instructions, which the model can overlook, hooks are deterministic — the harness executes them every single time.

Or skip the work: Meta-Prompt + System Prompt Architect does it in seconds →

You put “always run prettier after editing” in your CLAUDE.md. Claude does it… most of the time. Then one long session it edits nine files, formats six, and you find the mess in the diff. That gap — between a rule the model usually follows and a rule that always runs — is exactly what hooks close.

What Claude Code hooks actually are

Hooks are shell commands you define that Claude Code runs automatically at lifecycle events — before a tool call, after one succeeds, when you submit a prompt, when a session starts, when Claude finishes responding.

The key word is deterministic. Instructions in a CLAUDE.md file are suggestions the model reads and usually honors — but it can overlook one mid-task. A hook isn’t read by the model at all. The harness executes it, every time the event fires, no judgment involved. Rules become guarantees.

That’s the whole mental model: CLAUDE.md for judgment calls, hooks for laws.

Where hooks live

Hook config goes in your settings JSON, and there are three layers:

  • ~/.claude/settings.json — user-level, follows you across every project.
  • .claude/settings.json — project-level, committed to the repo so your whole team gets the same guardrails.
  • .claude/settings.local.json — project-level but personal; gitignored.

Don’t want to hand-edit JSON? Run /hooks inside a Claude Code session and it walks you through setting them up interactively. I’d still learn the raw shape, though, because that’s what you’ll be debugging later.

The config shape

Here’s a working starting template — a hook that auto-formats any file Claude edits:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write \"$(jq -r '.tool_input.file_path')\""
          }
        ]
      }
    ]
  }
}

Three parts to understand:

  • The event (PostToolUse) — when this fires. The main ones: PreToolUse (before a tool call — this one can block it), PostToolUse (after a tool call succeeds), UserPromptSubmit (when you submit a prompt), SessionStart, Stop (when Claude finishes responding), Notification, PreCompact, and SubagentStop.
  • The matcher ("Edit") — which tool triggers it, for the tool events. Tool names like Edit or Bash, patterns like "Edit|Write" to catch both, or an empty matcher to fire on every tool.
  • The hooks list{ "type": "command", "command": "..." } entries, the actual shell commands to run.

How a hook sees what’s happening

Every hook receives the event’s data as JSON on stdin. For tool events that includes the tool name and its input — the file path being edited, the bash command about to run. jq is the standard way to pull fields out:

jq -r '.tool_input.file_path'   # the file an Edit is touching
jq -r '.tool_input.command'     # the command a Bash call is running

That’s what the template above is doing — reading the edited file’s path off stdin and handing it to prettier.

Exit codes: the control channel

Your hook talks back to Claude Code through its exit code:

  • Exit 0 — continue normally. For UserPromptSubmit and SessionStart hooks, whatever the hook prints to stdout gets added into Claude’s context — which means a hook can inject information at the top of a session.
  • Exit 2block the action. The tool call doesn’t happen, and whatever you printed to stderr is fed back to Claude so it knows why and can adjust course instead of retrying blindly.

Exit 2 is the power move. It’s the difference between “please don’t touch that folder” and a bouncer at the door.

Four recipes worth stealing

1. Auto-format after every edit. The template above. Swap prettier for black, gofmt, whatever your stack formats with. You’ll never review an unformatted diff again.

2. Block edits to protected paths. A PreToolUse hook with matcher "Edit|Write" that reads the file path from stdin and exits 2 if it matches something sacred:

#!/bin/bash
path=$(jq -r '.tool_input.file_path')
if echo "$path" | grep -qE '\.env|migrations/'; then
  echo "Blocked: $path is protected. Edit the source template instead." >&2
  exit 2
fi
exit 0

Save that as a script, point the hook’s command at it. The stderr message matters — Claude reads it and reroutes instead of slamming into the wall repeatedly.

3. Audit-log every Bash command. A hook matching Bash that appends jq -r '.tool_input.command' to a log file. When you’re running Claude with broad permissions, that trail is how you sleep at night.

4. Desktop notification when Claude needs you. A Notification event hook that fires your OS’s notifier. You stop babysitting the terminal and go do something else until it pings you.

Two warnings before you go hook-happy

Hooks run with your user’s full permissions. The harness executes your command as you — a buggy hook can do real damage, same as any shell script with your keys. Keep commands simple, and test every hook standalone in a terminal before wiring it into settings.

Hooks add latency on every matching event. An empty matcher on PreToolUse means your script runs before every single tool call. Keep hooks fast and scoped to the tools they actually care about.

Why this matters more on cheap models

Here’s the un-obvious payoff. Deterministic enforcement matters most when the model is weakest. I run a lot of sessions against GLM models to keep costs near zero — setup is in how to set up Claude Code with the GLM API — and a lighter model drifts from soft instructions more than a frontier one does. Hooks don’t care. The formatter runs, the protected path stays blocked, the audit log fills up, regardless of what the model remembered. The plan I use for that is the z.ai GLM Coding Plan — full disclosure, https://z.ai/subscribe?ic=BWTG6TRYYQ is a referral link that helps fund our compute; same plan either way, just through our link.

Hooks plus a cheap backend is the combo: the guardrails are free and mechanical, so the tokens only pay for actual thinking.

Where this fits

Hooks are the enforcement layer of a Claude Code setup, and they slot alongside the other layers. Your CLAUDE.md holds the judgment-call rules and project facts; custom slash commands turn your repeated workflows into one-word triggers; hooks make the non-negotiables actually non-negotiable. Wire the three together and the tool stops needing supervision.

The remaining variable is instruction quality — hooks enforce the hard rules, but everything else still rides on how well you prompt. Meta-Prompt Architect is the pack I use to turn rough intent into reusable, high-signal instructions, so the tokens I’m paying for (referral-funded or not) do real work instead of thrashing.

Frequently asked

What's the difference between a hook and a CLAUDE.md instruction?

A CLAUDE.md instruction is a request to the model — it usually follows it, but it can overlook a rule mid-task, especially in long sessions. A hook is executed by the Claude Code harness itself at a lifecycle event, every time, no exceptions. Use CLAUDE.md for judgment calls and conventions; use hooks for anything that must never be skipped, like formatting, protected paths, or audit logging.

How do I block Claude Code from editing certain files?

Register a PreToolUse hook with a matcher like "Edit|Write", read the target file path from the JSON on stdin (jq -r '.tool_input.file_path'), and exit with code 2 if the path matches something protected. Exit 2 blocks the tool call entirely, and whatever you print to stderr is fed back to Claude so it knows why and can route around it instead of retrying blindly.

Where does hook configuration live — per project or globally?

Three places, same shape: ~/.claude/settings.json applies to you across every project; .claude/settings.json in a repo is project-level and meant to be committed so your team shares it; .claude/settings.local.json is project-level but personal and gitignored. If you'd rather not hand-edit JSON, the /hooks slash command inside a session walks you through setup interactively.

Are Claude Code hooks safe to run?

Hooks run with your user account's full permissions — the harness executes your command as you, so a buggy hook can delete files or leak data just like any shell script. Keep hook commands short, test them standalone in a terminal before wiring them in, and remember they also add latency on every matching event, so keep them fast.

Some links may be referral links, always marked. Full disclosure →