← All reviews
Field guide · May 28, 2026 · 8 min read

Claude Code hooks in 2026 — the four worth having, and the traps that bite

Open .claude/settings.json and you can hang far more off it than you’d expect. In 2026 Claude Code’s hook system grew from the old “run prettier on save” trick into a layer spanning close to thirty lifecycle events — session start and end, every prompt, before and after every tool call, permissions, compaction, subagents, worktrees. More power, more ways to wire it wrong. This is the short version: which hooks actually earn their keep, and the three traps that bite the people who go further.

What a hook actually is

A hook is a command — or an HTTP call, an MCP tool, even a subagent — that Claude Code runs synchronously when a lifecycle event fires. You configure it in a settings file, nested as event name → matcher → a list of handlers. The canonical shape:

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

The matcher is the tool name: Edit|Write matches either, a regex works too, and an empty string or * matches everything. The hook receives the tool call as JSON on stdin — format.sh reads the edited file’s path from it and formats it. When the event fires and the matcher hits, your command runs on your machine, with your credentials. Hold onto that last clause — we come back to it.

The four worth having

Out of the ~30 events, the ones that earn a place in day-to-day work are a short list:

  1. PostToolUse + Edit|Write → auto-format. The classic, and still the best-value hook there is. Claude writes a file, prettier / black / gofmt runs immediately, the tree stays clean without anyone thinking about it.
  2. PreToolUse + Bash → block dangerous commands. A script that inspects the command for rm -rf, git reset --hard, git push --force and denies it. A real safety rail — though the next section is about how it betrays you.
  3. SessionStart → inject context. Return additionalContext and you can hand Claude the current git branch, the environment, or a one-line repo overview at the top of every session.
  4. Stop / Notification → tell me it’s done. Claude finishes a turn, you get a desktop ping or terminal bell. Worth it the moment you start leaving long tasks running.

Everything else — worktrees, subagents, compaction, elicitation — exists for people writing plugins or orchestrating agent teams. For ordinary development, leave them alone. Every extra hook you add pays the three taxes below.

Trap 1: the blocking tax

Hooks are synchronous by default. A command hook’s timeout is 600 seconds — until it returns (or times out), Claude waits. And it runs every time the matcher hits; there’s no debounce. Hang a PostToolUse hook that shells out to an external linter on matcher: "Edit|Write" and you’ve added that linter’s runtime to every single file edit for the rest of the session.

It’s the same shape as a bloated CLAUDE.md being an invisible token tax on every session — except this one is a latency tax, billed per tool call. The fix is to narrow it: an if condition ("if": "Bash(git *)" runs only on git commands), or "async": true for hooks whose result you don’t need to wait on.

Trap 2: it fails open, not closed

This is the one that bites hardest. You write a PreToolUse hook to block rm -rf and feel safe. The happy path is fine: the matcher hits, your script exits with code 2 (or exits 0 with JSON permissionDecision: "deny"), the tool call is blocked.

But what if the script itself breaks — jq isn’t installed, a path is wrong, a typo? It exits with code 1 (or 127). And Claude Code treats any exit code that isn’t 0 or 2 as a non-blocking error: it shows one “hook error” line in the transcript and then runs the tool call anyway.

Read that again. The moment your safety hook breaks, the gate opens. It fails open, not closed. The rail you trusted to stop rm -rf is exactly the thing that lets it through — at the worst possible time, when the script crashed because someone’s environment differed from yours. Two disciplines follow: for a safety-critical hook, either guarantee the script can’t crash or explicitly exit 2 on every error path; and actually trigger a dangerous command once to confirm it gets blocked, rather than assuming. (This is the same class of failure as an AGENTS.md-only repo where Claude Code silently loads zero instructions and never errors: no error is not the same as working.)

Trap 3: it’s executable code in a shared file

.claude/settings.json is committed to the repo and shared across the team. The hooks in it run arbitrary shell with the runner’s credentials. Put those two facts together: a PostToolUse hook merged into main means every teammate, on every Edit from then on, is running a script someone else wrote into the config — and the UI doesn’t stop to ask first.

So a hook isn’t configuration. It’s code. Review it in the PR the way you’d review code, not the way you skim a settings line. (For where these .claude files actually live and who reads them, see what AI coding agents write to your disk.) On a team, managed settings can pin hooks to an admin-vetted set with allowManagedHooksOnly — worth it the moment more than a couple of people share the repo.

What we’d actually do

  • Solo: one or two hooks — format-on-write, maybe a done-notification. Skip the rest.
  • Safety rails: configure the PreToolUse blocker if you want one, but treat it as code and test it — trigger a command that should be blocked and confirm it fails closed.
  • Team: every hook in .claude/settings.json gets an owner, a PR review, and a periodic read-through alongside CLAUDE.md. What kills you isn’t any single hook — it’s settings.json and CLAUDE.md drifting apart with nobody responsible for either.

If your .claude directory has accumulated a pile of hooks nobody remembers the reason for, sitting next to a CLAUDE.md nobody’s sure is still accurate, that’s the knot the CLAUDE.md audit untangles — which hooks are helping, which are quietly taxing every call, and which have been failing open for months. $299 solo, $799 for a team of 2–10.

Companion reading

Related reading


Reviews independently produced · Editorial policy

Read more reviews →