← all posts
// workflow · claude-code

Hooks: deterministic guardrails for a probabilistic tool

You can write "always run the formatter after edits" into CLAUDE.md, and Claude Code will comply, most of the time. Most of the time is the problem. Instructions travel through a probabilistic model, and on turn forty, deep in a messy refactor, the model will sometimes skip the step it faithfully followed for the previous ten. Hooks exist to take that whole class of rule out of the model's hands.

A hook is a shell command bound to a lifecycle event in Claude Code: before a tool call, after one, on session start, when the agent stops and wants your attention. They live in your settings files (~/.claude/settings.json for you, .claude/settings.json for the project), each with a matcher scoping it to specific tools.

The shape of a hook

Here is the whole idea in one example: after every file edit, run the formatter. Not "please remember to." Just run it.

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{ "type": "command", "command": "npx prettier --write ." }]
    }]
  }
}

The event names tell you where you can stand in the loop. PreToolUse fires before a tool call and can veto it; PostToolUse fires after; there are events for session start, prompt submission, and the agent stopping, among others. The detail that makes this a guardrail rather than a tripwire: when a hook blocks an action, its message is fed back to the model, which reads the reason and adjusts course instead of flailing.

Never prompt for what you can enforce

That is the design principle, and it sorts your standing instructions into three piles.

  • Make it happen. Formatters, import sorters, codegen refresh: anything that must follow every edit. A PostToolUse hook on Edit|Write ends the debate permanently.
  • Make it impossible. Force-pushes, rm -rf, edits to generated files or locked migrations. A PreToolUse matcher on Bash inspects commands before they ever execute.
  • Make it visible. Desktop notifications when the agent needs input or finishes a long run, so a background session is never silently stuck.

If a rule matters, do not write it in the prompt. Wire it into the loop, where it cannot be forgotten.

Worth keeping crisp: hooks are not the permission system. Permissions decide whether the agent may use a tool at all; hooks decide what happens around each use, deterministically. They compose well (I cover the permission side separately), but hooks are where behavior stops depending on model mood.

What hooks cost you

Three honest caveats. First, hooks are arbitrary shell running with your full user permissions. A .claude/settings.json in a repo you just cloned is code you are about to execute; Claude Code asks before trusting new hook configs, but the actual reading is on you. Treat a stranger's hooks like a stranger's Makefile.

Second, slow hooks tax every matching tool call. A three-second formatter after each edit turns a twenty-edit refactor into a minute of dead air. Scope matchers tightly, keep commands fast, and prefer formatting the touched file over the whole tree.

Third, over-blocking makes the agent thrash. A hook that rejects without explanation leaves the model guessing, and it will retry variations until it stalls or gives up. Always emit a reason: the model can only adapt to what it can read. This is the same lesson output validation teaches everywhere else: reject with feedback, never silently.

Start with exactly two: your formatter on PostToolUse, and one PreToolUse blocker for the command that scares you most. That is maybe fifteen lines of JSON, and it converts your two most-repeated instructions from requests into physics. I know of no cheaper reliability win anywhere in this tool.

#claude-code#hooks#workflow