Blog · August 2, 2026 · 7 min read

Claude Code Hooks: A Practical Guide to Automating Your Agent Workflow

Everything you put in a prompt or a CLAUDE.md file is, in the end, a suggestion. Claude Code usually follows it — but "usually" is not a guarantee, and for some things you want a guarantee. Always run the formatter after edits. Never touch the production config. Tell me the moment you finish.

That's what hooks are for. A Claude Code hook is a shell command that runs automatically at a specific point in the agent's lifecycle — before a tool call, after a file edit, when the session ends, when Claude stops and waits for you. It's deterministic: the hook fires every single time the event happens, whether or not the model "remembers" to do it. Here's how to set them up, engineer to engineer, with the three hooks that pay off immediately.

Where hooks live

Hooks are plain JSON in your settings files. Three locations matter:

  • ~/.claude/settings.json — applies to every project on your machine.
  • .claude/settings.json — lives in the repo, so the whole team gets the hook when they pull.
  • .claude/settings.local.json — project-specific but gitignored, for personal stuff you don't want to commit.

The structure has three levels: the event (when it fires), an optional matcher (which tool triggers it), and one or more handlers (what runs). A minimal example:

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

That's a complete, working hook: every time Claude edits or writes a file, Prettier runs. No prompt engineering, no hoping the model remembers your style guide. Matchers accept exact tool names, pipe-separated lists like Edit|Write, or regex; leave the matcher out (or use *) to fire on everything.

Hook #1: auto-format after every edit

The example above is the classic first hook, and it's worth setting up today. PostToolUse fires after a tool call succeeds, and your hook receives a JSON payload on stdin describing exactly what happened — including tool_input.file_path, so you can format only the file that changed instead of the whole repo:

#!/bin/bash
FILE=$(jq -r '.tool_input.file_path')
[[ "$FILE" == *.ts || "$FILE" == *.tsx ]] && npx prettier --write "$FILE"
exit 0

Save that as a script, point the hook's command at it (the ${CLAUDE_PROJECT_DIR} placeholder resolves to your project root, so committed hooks work on everyone's machine), and formatting stops being something you review for.

Hook #2: block the commands you never want run

PreToolUse fires before a tool call, and this is where exit codes get interesting. A hook that exits with code 2 blocks the action, and whatever the script printed to stderr is fed back to Claude as the reason — so the agent doesn't just fail, it understands why and adjusts course:

#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -qE 'rm -rf|--force'; then
  echo "Destructive command blocked by policy" >&2
  exit 2
fi
exit 0

Wire that to PreToolUse with a Bash matcher and you have a guardrail that no amount of context drift can talk its way past. The exit-code contract is simple: 0 means proceed, 2 means block and explain, anything else is a non-blocking error that shows up in the transcript but doesn't stop the run. The same trick works for protecting files — match on Edit|Write and exit 2 when the path is .env, a lockfile, or a production config.

Hook #3: know the moment Claude stops needing you — or starts

The two events most people discover last are the ones with the biggest quality-of-life payoff. Stop fires when Claude finishes responding — the task is done. Notification fires when Claude Code is trying to get your attention, and it tells you why: the matcher value permission_prompt means Claude is blocked waiting for your approval, and idle_prompt means it's been sitting idle waiting for input.

Why does that matter? Because a real Claude Code task runs for minutes, and the expensive failure mode isn't the agent being slow — it's the agent being done (or blocked on a permission prompt) while you're off in another window. We wrote a whole post on not watching the terminal; hooks are the mechanism that makes it possible. A first version can be one line on macOS:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "afplay /System/Library/Sounds/Glass.aiff"
          }
        ]
      }
    ]
  }
}

That works, but you'll outgrow it fast: a sound doesn't tell you which session finished when you're running several in parallel, doesn't distinguish "done" from "waiting for permission", and isn't clickable. We built AI Done Now because we kept rebuilding this exact hook setup on every machine — it's a Mac menu bar app that handles the wiring for you and shows native, clickable notifications with distinct alerts for "finished" and "needs your input", for Claude Code and other agent CLIs.

Gotchas worth knowing before you ship a hook

A few things that save you a debugging session:

  • Hooks run with your full user permissions. They're arbitrary shell commands executing automatically — review any hook you copy from the internet, and be doubly careful with hooks committed to shared repos.
  • Set timeouts on slow commands. Command hooks get a generous default timeout (600 seconds), which means a hung script can stall your whole session. Add a per-hook timeout value for anything that touches the network.
  • Stop hooks can do more than notify. A Stop hook that exits 2 actually prevents Claude from stopping — people use this to enforce "don't finish until the tests pass". Powerful, but add an escape hatch or you can loop forever.
  • Read the payload from stdin. Every hook receives JSON describing the event — session ID, working directory, tool input. jq is your friend.

Start with one

Hooks reward incrementalism. Pick the one that stings most — formatting noise in diffs, a command you're scared of, or finding out ten minutes late that Claude finished — and wire up just that. Once the first one quietly does its job for a week, you'll know exactly which hook to add next. And if the one you want is "tell me when it's done", that's the one you don't have to build: AI Done Now handles Claude Code notifications out of the box.

Keep reading