Back to blog
Aug 20, 2026 · 3 min read

Conventions Are Advice. Hooks Are Guarantees.

A rule in a markdown file is followed most of the time, which is fine for a colleague and useless for a build step. Hooks are your own code running on the agent’s tool calls, which turns a convention into something that happens whether or not anyone remembered it.

Claude CodeHooksAutomationWorkflow

Write “always run the formatter after editing” in a project file and it will happen most of the time. Most of the time is a wonderful property for a colleague and a terrible one for a build step, and the gap shows up as review comments about whitespace, which is the least interesting way to spend a review.

Hooks close that gap by moving the rule out of the conversation entirely.

the same rule, two places to put it
AdviceGuarantee
what it is“always format after an edit”a PostToolUse hook
lives inCLAUDE.mdsettings.json
runs whenrememberedevery time
coststokens, every turnnothing
failssilentlyloudly
If a rule matters more than it costs to enforce, it should not be a sentence.

Where a hook gets a say

A hook is a shell command Claude Code runs at a fixed point in its own lifecycle. Your script, your machine, and its output comes back to the agent as feedback, which is how a hook refuses something and explains itself in the same breath.

the tool call lifecycle
  1. 01 You ask Something you want done.
  2. 02 A tool is chosen PreToolUse runs here. Your script sees the call first and can refuse it, with a reason.
  3. 03 The tool runs Bash, Edit, Write, and the rest. PostToolUse runs after it: formatters, codegen, the lint you keep forgetting.
  4. 04 The result is read Claude reads what came back and carries on.
  5. 05 It tries to finish Stop runs here, and can send it back to work.
PreToolUse can block. PostToolUse reacts. Stop can refuse the ending.

They are configured in settings.json and matched against the tool being called, so a hook can apply to every edit, or only to Bash, or only to a subset you match with a pattern.

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/format-changed.sh" }
        ]
      }
    ]
  }
}

The hook receives the tool call as JSON on standard input, so the script knows exactly which file was written and can format only that one instead of the whole tree. Keep the script in the repo next to the config; a hook that shells out to a one liner is a hook nobody can debug in six months.

Three hooks worth having

  1. Format on write. The classic, and the one that deletes a whole category of review comment. PostToolUse on Edit|Write.
  2. Refuse a command class. A PreToolUse hook on Bash that reads the command and exits non-zero for the ones you never want run unattended. This is a real guarantee, unlike a line in CLAUDE.md asking politely.
  3. Rewrite a command into the one you meant. If every git call in your project should really go through a wrapper, a hook can do that substitution every single time, with no cooperation from the model and no tokens spent explaining it.

That third one is worth dwelling on, because it is the pattern people miss. Anything you find yourself correcting in the transcript, over and over, is a candidate for silent substitution. You are not teaching the agent a preference; you are removing the choice.

Which settings file

There are three, and the difference is a team question rather than a technical one.

  • ~/.claude/settings.json follows you into every project. Your habits live here.
  • .claude/settings.json is committed. Team conventions live here, and everyone gets them on clone.
  • .claude/settings.local.json is gitignored. Your personal permission allowlist for this repo lives here, because nobody else wants it.

A hook is code, with no model in the loop

This is the part to take seriously. A hook runs on every matching event, immediately, with no judgement applied. A PostToolUse hook with a bad path will happily corrupt every file the agent touches, quickly, and the agent will keep working on the wreckage because as far as it knows the write succeeded.

  • Run the command by hand before you wire it to an event. Every time.
  • Make it a no-op when it does not apply, rather than an error, or you will spend the session reading hook failures.
  • Keep it fast. It runs on every matching tool call, and a two second hook on a busy session is a tax you will feel.
  • Log what it did somewhere you can read afterwards.

The Stop hook is the interesting one

PreToolUse and PostToolUse are about individual actions. The Stop hook is about the ending: it runs when the agent tries to finish, and it can send it back to work. That is a different kind of power, because it is the only one that can enforce a definition of done rather than a definition of behaviour.

It is also, not coincidentally, the mechanism behind the goal command in the last post of this series. Once you have seen the hook, that feature stops looking like magic and starts looking like a gate you could have built yourself.

Copied to clipboard