// claude code · guide

Claude Code hooks: what they are, with 4 examples to copy

Short answer: a Claude Code hook is a small script Claude Code runs at a fixed moment: before Claude uses a tool, after it edits a file, or when it needs you. CLAUDE.md asks; a hook enforces. It runs every time, whatever Claude decides, and when it runs before an action, exit code 2 blocks it. The four I'd add first: keep Claude out of .env, format every edit, keep commits off main until the tests pass, and get a ping when Claude waits for you.

CLAUDE.md asks, a hook enforces

Everything in CLAUDE.md is a request. Anthropic's memory docs say Claude treats it as "context, not enforced configuration", and point to a PreToolUse hook when an action must be blocked whatever Claude decides.

Most rules can live with that: if Claude forgets your naming style once, you fix it in review. Some can't: don't touch the keys, don't ship untested code. My rule of thumb: if a slip is cheap, write the rule in CLAUDE.md. If it can hurt the live app, make it a hook.

My free MVP System Design playbook is the first kind: plain instructions, no code to run. A hook is code on your computer, so this guide ends with a warning.

How a hook works

A hook has three parts: the event (when it runs), the matcher (which tool it watches) and the command (what runs). Four of the 30-plus events cover most needs:

EventWhen it runsCan it block?
PreToolUsebefore Claude uses a tool: edits a file, runs a commandyes
PostToolUseafter a tool call succeedsno
Notificationwhen Claude is waiting for your input or permissionno
Stopwhen Claude finishes respondingyes: Claude goes back to work

For tool events, the matcher is the tool's name: Bash for shell commands, Edit|Write for file edits, empty for everything. It is case-sensitive.

Claude Code hands the hook the details, such as the file path or the command, as JSON, a plain-text data format. The scripts below read it with jq: install it first, with brew install jq on a Mac. The script answers with its exit code, the number a program returns when it ends:

Exit codeWhat happens
0no objection: the action goes ahead as usual
2block: a PreToolUse hook cancels the action, in every permission mode, and Claude gets the script's error output (stderr) as the reason
anything elsean error notice, and on most events the action still goes ahead

Project hooks go in .claude/settings.json, committed, for everyone on the repo. Personal ones go in ~/.claude/settings.json, for all your projects. The facts here are from Anthropic's hooks guide and hooks reference, checked on 7 November 2026. Hooks 1, 2 and 4 are their examples; hook 3 is mine.

Hook 1: keep Claude out of .env

Why it matters: .env holds your passwords and keys. Keep changes to it in your hands: one wrong edit can cut your app off from its database or payments.

Save Anthropic's script as .claude/hooks/protect-files.sh. It checks the file Claude wants to edit against a list and exits with 2 on a match:

#!/bin/bash
# protect-files.sh

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Normalize Windows backslash separators so the patterns below match
FILE_PATH="${FILE_PATH//\\//}"

PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")

for pattern in "${PROTECTED_PATTERNS[@]}"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done

exit 0

Make it runnable with chmod +x .claude/hooks/protect-files.sh, then register it in .claude/settings.json. $CLAUDE_PROJECT_DIR is your project's root folder:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}

Test it: ask Claude to add a comment to .env, and Claude Code should block the edit. One limit: it watches Claude's edit tools, so a shell command that writes to the file slips past. Anthropic's guide covers that case too.

Hook 2: format every file Claude edits

Why it matters: every file comes out in the same style, so a pull request shows only real changes, and review goes faster.

After each edit, this runs Prettier, a code formatter, on the file. Add it to .claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

It needs Node.js. If Prettier doesn't cover your language, swap in its formatter. A PostToolUse hook can't undo an edit, only tell Claude about it, so guards belong in PreToolUse.

Hook 3: no commits to main, and tests first

Why it matters: in most setups, main is what runs in production. A commit straight to it skips review, and untested code is one deploy away from your users.

This script checks every shell command Claude runs. It blocks git commit and git push on main or to it, and runs your tests before any other commit or push. Save it as .claude/hooks/protect-main.sh and chmod +x it:

#!/bin/bash
# protect-main.sh

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
DIR=$(echo "$INPUT" | jq -r '.cwd // empty')

# Only commits and pushes are checked
if [[ "$COMMAND" != *"git commit"* && "$COMMAND" != *"git push"* ]]; then
  exit 0
fi

# 1. Nothing on main or to main ("main" as a whole word,
#    so a branch like feature-main is not blocked)
BRANCH=$(git -C "$DIR" branch --show-current)
MAIN_RE='(^|[[:space:]:])main([[:space:]]|$)'
if [[ "$BRANCH" == "main" ]] ||
   [[ "$COMMAND" == *"git push"* && "$COMMAND" =~ $MAIN_RE ]]; then
  echo "Blocked: no commits or pushes to main. Use a branch and a pull request." >&2
  exit 2
fi

# 2. Tests must pass (use your own test command)
cd "$DIR" || exit 2
if ! npm test >&2; then
  echo "Blocked: the tests fail. Fix them, then commit." >&2
  exit 2
fi

exit 0

Register it on the Bash tool. If you use hook 1 too, put both groups side by side in the same PreToolUse list:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-main.sh"
          }
        ]
      }
    ]
  }
}

The tests print to the error output, so failing tests reach Claude as the reason for the block. My playbook enforces 99% test coverage, and this hook runs the tests before every commit. Main branch called master? Change the name. The hook reads the command Claude types, not what other scripts do, so also lock main on GitHub with a branch protection rule.

Hook 4: a notification when Claude needs you

Why it matters: Claude often stops to ask your permission, then waits until you look. A ping lets you work on something else.

This one is about you, not the project, so it goes in ~/.claude/settings.json. The macOS version:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

The guide has Linux and Windows versions too. The empty matcher fires on every notification; permission_prompt|idle_prompt narrows it to "needs your approval" and "has waited a minute for you". Nothing appears on a Mac? Allow notifications for Script Editor in System Settings.

Read every hook before you paste it

A hook runs with your full user permissions: as Anthropic's reference warns, it can change, delete or read any file your account can. So:

Next: Claude Code skills and my Claude Code workflow. All guides: articles.

// next step

Get the playbook

The MVP System Design skill: the rules your AI agent follows from an empty repo to production, in 16 chapters. Plain instructions, no code to run. Free.