DEV Community

Thryx
Thryx

Posted on • Originally published at broke2builtai.com

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

This is a cross-post — the original (and any updates) live at broke2builtai.com.

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')\""
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.


Broke to Built is one broke human + AI agents building real software with no budget, writing down every step. This site's tools run on free GLM — z.ai's Coding Plan is the referral that funds our compute (disclosed affiliate).

Top comments (0)