Home / Guides / Claude Code hooks examples

Claude Code hooks examples: 7 you can copy today

Claude Code hooks are shell commands that run at fixed points in a session, so a rule happens every time instead of whenever the model remembers. Below are seven copy-paste examples for settings.json: format on edit, protect secrets, block destructive Bash, notify, restore context after /compact, tests before stop, and a command log.

Every example follows the shapes in Anthropic's hooks guide and hooks reference (checked October 21, 2026). The Bash examples use jq.

How a hook works in 30 seconds

1. Auto-format every file Claude edits

PostToolUse with an Edit|Write matcher runs after each file edit. The hook gets the tool call as JSON on stdin, so jq pulls out the path. Put it in the project's .claude/settings.json.

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

Swap Prettier for ruff format, gofmt -w or your formatter. Don't block from a PostToolUse hook: the edit already happened, and the docs note PostToolUse hooks can't undo actions.

2. Block edits to .env, lockfiles and .git/

A PreToolUse hook runs before the edit. Exit code 2 blocks it and sends your stderr message to Claude as feedback, so it can change approach.

.claude/hooks/protect-files.sh
#!/bin/bash
# PreToolUse, matcher Edit|Write. Exit 2 = block; stderr goes to Claude.
FILE_PATH=$(jq -r '.tool_input.file_path // empty')
for pattern in ".env" "package-lock.json" ".git/"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done
exit 0

Run chmod +x .claude/hooks/protect-files.sh, then register it:

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

This pattern also matches .env.example. If you want Claude to edit that, check for it first and exit 0.

3. Block rm -rf and force-push in Bash

Same idea on the Bash tool, reading tool_input.command. The if field (permission-rule syntax) can narrow a hook to specific commands, for example "if": "Bash(git *)", so the script only spawns for git.

.claude/hooks/block-dangerous-bash.sh
#!/bin/bash
# PreToolUse, matcher Bash
CMD=$(jq -r '.tool_input.command // empty')
if echo "$CMD" | grep -qE 'rm -rf (/|~|\*|\.)|git push (-f|--force)|git reset --hard'; then
  echo "Blocked: destructive command. Explain why it is needed and ask the human to run it." >&2
  exit 2
fi
exit 0

The docs call the if filter best-effort and recommend the permission system for a hard allow or deny, so pair this with permissions.deny rules. The full version we ship for free is on the subagents and hooks guide.

4. Desktop notification when Claude needs you

Notification fires when Claude is waiting for input or permission. An empty matcher fires on every notification type. This one goes in ~/.claude/settings.json so it works in every project (macOS shown; use notify-send on Linux).

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

Narrow it with a matcher such as permission_prompt or idle_prompt if you only want some alerts.

5. Re-inject key context after /compact

Compaction summarizes the conversation and can drop details. A SessionStart hook with the compact matcher runs after compaction, and Claude Code adds the command's plain-text stdout to Claude's context.

.claude/settings.json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          { "type": "command", "command": "echo 'Reminder: use pnpm, not npm. Run pnpm test before saying done.'; git log --oneline -5" }
        ]
      }
    ]
  }
}

For rules that should load in every session, put them in CLAUDE.md instead: see the CLAUDE.md examples and template.

6. Run tests before Claude stops

A Stop hook fires when Claude finishes responding. Exit 2 with a reason keeps it working. Always check stop_hook_active first: Claude Code overrides a Stop hook that blocks eight times in a row, but you don't want to get there.

.claude/hooks/tests-before-stop.sh
#!/bin/bash
# Stop hook. Exit 2 = keep working; stderr tells Claude why.
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
git diff --quiet && git diff --cached --quiet && exit 0   # nothing changed
if ! npm test --silent >/tmp/claude-stop-tests.log 2>&1; then
  echo "Tests are failing. Fix them before finishing. See /tmp/claude-stop-tests.log" >&2
  exit 2
fi
exit 0

Register it under "Stop" with no matcher (Stop doesn't support one). For the full tests-gate version (loop guard, dirty-tree skip, pytest/npm wiring), see A Stop hook that runs tests.

7. Log every Bash command

An audit trail in one line. PostToolUse on Bash appends what ran to a file in your home directory.

~/.claude/settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt" }
        ]
      }
    ]
  }
}

When a hook doesn't fire

Hooks run with your user's permissions. Read every script before you enable it.

FAQ

Where do Claude Code hooks go?

In a settings file under a top-level hooks key: .claude/settings.json for the project, .claude/settings.local.json for just you in that project, or ~/.claude/settings.json for all your projects. Run /hooks to browse them.

How do I block a command with a Claude Code hook?

Use a PreToolUse hook with matcher Bash. Read tool_input.command from the JSON on stdin and exit with code 2 to block; whatever you print to stderr is sent to Claude as the reason.

Does exit 0 approve a tool call?

No. Exit 0 from a PreToolUse hook means no objection, and the normal permission flow still applies. To approve explicitly, exit 0 and print JSON with permissionDecision set to allow.

How do I run tests before Claude Code finishes?

Add a Stop hook that runs your tests and exits 2 with a message when they fail. Check stop_hook_active in the input and exit 0 when it is true so the hook doesn't loop.

Can a hook re-add context after /compact?

Yes. A SessionStart hook with the compact matcher runs after compaction, and plain text it prints to stdout is added to Claude's context.

Want the whole drawer?

The Claude Code Kit ($24) ships a fill-in CLAUDE.md, 6 least-privilege subagents, 10 slash commands, a settings.json with permissions and hook wiring, and 4 tested hooks (dangerous Bash, secret files, lockfiles, format, tests-before-stop).

Not ready for a full pack? Start with One-File Agent Rules ($9), one universal AGENTS.md for any stack, or get the free Starter AGENTS.md by email.

Launch week: 30% off any kit with code LAUNCH at Stripe checkout, through Oct 28, 2026. 14-day refund.

FREE · 1 FILE · WORKS IN EVERY AGENT

Get the free Starter AGENTS.md

A 40-line rules file that makes Cursor, Claude Code, Codex and Windsurf plan first, keep diffs small and run your tests before saying "done". We email it to you instantly, plus a short note when a new pack ships. Unsubscribe anytime.