Agent Shipped

Guide · updated 2026-10-12 · 7 min read

Claude Code Hooks: 8 Copy-Paste Examples

Claude Code hooks are shell commands (or HTTP calls, MCP tool calls, prompts or agents) that run automatically at fixed points in a session, like before a tool runs, after a file edit, or when Claude finishes responding. You add them to a hooks block in ~/.claude/settings.json, .claude/settings.json or .claude/settings.local.json, and the eight examples below cover the jobs most people actually need: formatting, blocking risky edits, notifications, test gates, context injection and audit logs.

Every example here comes from, or is built directly on, the official hooks guide and hooks reference. Run /hooks after adding one to confirm it registered.

How hooks work in 60 seconds

The config shape is always the same: an event name, an optional matcher that filters when it fires, and a list of handlers.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "your-command-here" }]
      }
    ]
  }
}

Claude Code sends event data as JSON on stdin. For a PreToolUse hook on Bash that looks like:

{
  "session_id": "abc123",
  "cwd": "/Users/sarah/myproject",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "npm test" }
}

Your script answers with an exit code:

The events you'll use most are PreToolUse, PostToolUse, Stop, SessionStart, UserPromptSubmit and Notification. The full list is in the hooks guide.

Most examples parse JSON with jq. Install it first (brew install jq or apt-get install jq).

1. Desktop notification when Claude needs you

Add to ~/.claude/settings.json. macOS:

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

On Linux swap the command for notify-send 'Claude Code' 'Claude Code needs your attention'. An empty matcher fires on every notification type. Narrow it with permission_prompt (an approval has waited about six seconds) or idle_prompt (Claude finished about 60 seconds ago and you haven't typed).

If nothing appears on macOS, the docs note that osascript routes through Script Editor, which needs notification permission in System Settings.

2. Auto-format every file Claude edits

Add to .claude/settings.json so the whole team gets it:

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

Swap in ruff format, gofmt -w or whatever your stack uses. A successful hook prints nothing, so check the file to confirm it ran. Note that this only catches edits made through Claude's file tools. If Claude rewrites a file with a Bash command, the docs suggest a FileChanged hook or a Stop hook that scans the working tree.

3. Block edits to protected files

Save as .claude/hooks/protect-files.sh:

#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
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 executable with chmod +x .claude/hooks/protect-files.sh, then register it:

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

Test it by asking Claude to add a comment to .env. Claude gets the Blocked: message and adjusts. Pair this with "Read(./.env)" in permissions.deny if you also want to stop Claude reading the file.

4. Block dangerous shell commands

Same pattern, aimed at Bash. The docs' minimal version:

#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')

if echo "$COMMAND" | grep -q "drop table"; then
  echo "Blocked: dropping tables is not allowed" >&2
  exit 2
fi

exit 0

To avoid spawning the script for every Bash call, use the if field, which takes permission-rule syntax and only runs the hook when the command matches:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git *)",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
          }
        ]
      }
    ]
  }
}

The if filter is best-effort, and the docs say to use permission rules for a hard allow or deny. What makes hooks worth it here: a PreToolUse hook that denies a call blocks it even in bypassPermissions mode or with --dangerously-skip-permissions.

5. Make Claude run the tests before it stops

A Stop hook fires whenever Claude finishes responding. Returning a block makes it keep going. Here's a command-hook version that runs your tests and only lets Claude stop when they pass:

#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi

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). The stop_hook_active check matters: it's true when Claude is already continuing because of a stop hook, and the docs tell you to exit early in that case. Claude Code also overrides a Stop hook after eight consecutive blocks with no tool call in between.

If you'd rather let a model judge, the docs show an agent hook that runs the suite itself:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "agent",
            "prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

Agent hooks are marked experimental, so prefer the command version for anything you depend on.

6. Re-inject context after compaction

When the context fills up, compaction summarizes the conversation and details can get lost. A SessionStart hook with the compact matcher puts critical reminders back:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Reminder: use Bun, not npm. Run bun test before committing. Current sprint: auth refactor.'"
          }
        ]
      }
    ]
  }
}

Replace the echo with git log --oneline -5 for something dynamic. For context you want in every session, use CLAUDE.md instead.

7. Add live context to every prompt

A UserPromptSubmit hook can attach information to each prompt by printing JSON with additionalContext nested inside hookSpecificOutput:

{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "Current branch: release-42. Deploy freeze until Friday."
  }
}

Put additionalContext at the top level and Claude Code silently ignores it. That's the most common reason a JSON hook "does nothing."

8. Log every command Claude runs

A simple audit trail, in ~/.claude/settings.json:

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

For a shared audit service, an HTTP hook POSTs the same JSON to an endpoint ("type": "http", with url, headers and an allowedEnvVars list for any $VAR in headers). To log config edits during a session, the ConfigChange event works the same way.

Where to put hooks

Location Scope
~/.claude/settings.json All your projects
.claude/settings.json This project, committed
.claude/settings.local.json This project, just you
Managed policy settings Organization-wide
Plugin hooks/hooks.json When the plugin is enabled
Skill or subagent frontmatter While that skill or subagent is active

If a settings file already has a hooks key, add your event as a sibling inside it rather than adding a second hooks object. To turn everything off temporarily, set "disableAllHooks": true.

Troubleshooting

Command hooks run with your full user permissions, so review any hook script in a repo you clone before you trust the folder.

Bottom line

Start with two hooks: a notification so you can walk away, and a formatter on Edit|Write so diffs stay clean. Add a PreToolUse guard for the files and commands that would ruin your day, and a Stop gate once you trust your test suite. Anything you've written in CLAUDE.md three times and Claude still skips is a hook waiting to be written. More on combining hooks with the rest of your setup in Claude Code best practices.

Subscribe to Agent Shipped for a new tested hook, skill or workflow twice a week.

FAQ

Where do Claude Code hooks go?

In a hooks block inside a settings file. Use ~/.claude/settings.json for hooks that apply to all your projects, .claude/settings.json for hooks you commit and share with your team, and .claude/settings.local.json for project hooks you keep to yourself.

How does a hook block an action?

Exit with code 2 and write the reason to stderr. On PreToolUse that cancels the tool call and passes your message to Claude as feedback. For more control, exit 0 and print a JSON object with a permissionDecision of deny, allow or ask.

Why isn't my hook firing?

Run /hooks to check it is registered under the right event. Matchers are case-sensitive, scripts must be executable on macOS and Linux, and any text your shell profile prints before your JSON stops Claude Code from parsing it.

Are hooks better than CLAUDE.md instructions?

For rules that must hold every time, yes. CLAUDE.md is guidance Claude tries to follow, while hooks run deterministically at fixed lifecycle events whatever Claude decides.

Get the field guide

Join builders who use coding agents every day. Free, twice a week, unsubscribe in one click.

No spam. You get the CLAUDE.md starter pack in your welcome email.

Keep reading