Post your build ideas
LocalASO Track App Store keyword ranks in every countryFind your vibe. Find you job which is just for you and your dataDealtab Brand Deal Tracker That Does The Work For YouIs Major Cooked? Free AI risk score + roast for your college majorWiral.Social LinkedIn, Scheduler, Scheduling Tool, Ghostwriter
Solo Era solo activities, solo maxxing, sidequests, challengeCadentiq daily routine, wellbeing, self care, healthy habitsTexeva Your businessKeepsake Project Management, Freelancers, DesignersOrvyx AI You create

Claude Code hooks: events, settings.json and working examples

Claude Code hooks are shell commands, HTTP calls or prompts that run automatically at set points in a session, such as before a tool runs or when Claude finishes responding. You define them in a settings.json file under a hooks key: an event, a matcher, then a handler. A hook that exits with code 2 blocks the action, so hooks enforce rules that CLAUDE.md can only request.

What are Claude Code hooks?

A hook is a handler that Claude Code runs when something happens in a session: a prompt is submitted, a tool is about to run, a file edit finished, Claude stopped to wait for you. When the event fires and a matcher fits, Claude Code passes JSON about the event to your handler. For a command hook that JSON arrives on stdin. Your handler can then inspect it, take action, and optionally return a decision.

The reason to use them is determinism. The official docs are explicit that Claude treats CLAUDE.md as context, not enforced configuration, and recommend a PreToolUse hook when an action must be blocked regardless of what Claude decides. An instruction asks. A hook acts. If you want Prettier to run after every edit, or .env never to be touched, write a hook; if you want Claude to prefer one library over another, write it in CLAUDE.md.

CLAUDE.md, skills and hooks compared
MechanismHow reliableGood for
CLAUDE.mdContext Claude tries to followConventions, preferences, project facts
SkillInstructions Claude applies with judgmentRepeatable procedures you invoke
HookCode that runs every time its event firesFormatting, blocking, logging, notifications, checks

See the guide to writing CLAUDE.md and the guide to Claude Code skills for the other two.

Which hook events are there?

The hooks reference lists more than thirty events. Most people use six or seven. Events fall into three rhythms: once per session, once per turn, and on every tool call inside the agent loop.

The Claude Code hook events most people use
EventWhen it firesCan block?
SessionStartA session begins or resumesNo
UserPromptSubmitA prompt is submitted, before Claude processes itYes
PreToolUseBefore a tool call runsYes
PermissionRequestA tool call needs a permission decisionThrough a decision object, not exit 2
PostToolUseAfter a tool call succeedsNo, the tool already ran
PostToolUseFailureAfter a tool call failsNo
NotificationClaude Code sends a notification, such as waiting for inputNo
StopClaude finishes respondingYes, keeps Claude working
SubagentStopA subagent finishesYes
PreCompactBefore context compactionYes
SessionEndA session endsNo

Others cover subagent start, task creation, config changes, a changed working directory, watched file changes, worktree creation, model switches and MCP elicitation. Use /hooks inside Claude Code to see what is configured.

One trap: Stop fires whenever Claude finishes responding, not only when the whole task is done, and it does not fire if you interrupt. Keep Stop hooks fast.

What is the settings.json format for hooks?

Hooks live in JSON settings files and have three levels of nesting. First, choose an event. Second, add a matcher group that filters when it fires. Third, define one or more handlers to run. Each event name is a key inside a single hooks object, so a new event is a sibling of the existing ones.

Hook structure: event, matcher group, handler
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command", "command": "notify-send 'Claude Code' 'Claude Code needs your attention'" }
        ]
      }
    ]
  }
}
Where to define hooks
LocationScopeShareable?
~/.claude/settings.jsonAll your projectsNo, local to your machine
.claude/settings.jsonOne projectYes, commit it to the repository
.claude/settings.local.jsonOne project, just youNo, kept out of git
Plugin hooks/hooks.jsonWhile the plugin is enabledYes, bundled with the plugin
Skill or subagent frontmatterWhile the skill or subagent is activeYes, defined in the file

Hook entries merge across levels rather than replacing each other. Use the project file for rules the whole team needs, such as formatting, and the user file for personal ones, such as notifications.

The five hook handler types
TypeWhat it does
commandRuns a shell command; input arrives on stdin; results come back through exit codes and stdout
httpPOSTs the event JSON to a URL and reads the decision from the response
mcp_toolCalls a tool on a configured MCP server
promptSends a prompt to a Claude model for a single-turn yes or no evaluation
agentSpawns a subagent that can read files to verify a condition. Experimental

Common handler fields are type, timeout in seconds (default 600 for command hooks), statusMessage for a custom spinner line, and if, which narrows a hook with a single permission rule such as "Bash(git push *)". Handlers for one event run in parallel.

How do matchers work?

The matcher filters when a group fires. What it matches depends on the event: tool events match the tool name, SessionStart matches how the session started (startup, resume, clear, compact), and Notification matches the notification type.

Matcher values and how they are read
MatcherMeaningExample
"*", "" or omittedMatch everythingFire on every occurrence
Letters, digits, _, -, |, , onlyExact name, or a list of exact namesBash, Edit|Write
Anything elseA JavaScript regular expression, unanchored^Notebook, mcp__memory__.*

Matchers are case-sensitive, and a miss is the most common reason a hook never fires. Wrap a regex in ^ and $ when you need a whole-string match: Edit.* also matches NotebookEdit.

How does a hook read input and block an action?

A command hook gets JSON on stdin. For a PreToolUse hook on a Bash command it looks like this, trimmed here to the useful fields:

What a PreToolUse hook receives on stdin (trimmed)
{
  "session_id": "abc123",
  "cwd": "/home/user/my-project",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "npm test" }
}

Your script answers with its exit code. These are the three outcomes:

Hook exit codes
Exit codeMeaningWhat happens
0SuccessThe action goes ahead. Any JSON you print is applied. For UserPromptSubmit, SessionStart and a few others, plain-text stdout is added to Claude's context
2Blocking errorOn events that can block, the action stops and your stderr becomes the reason Claude sees
Any otherNon-blocking errorThe action goes ahead and a hook error notice shows the first line of stderr

What exit 2 does depends on the event. On PreToolUse it blocks the tool call. On UserPromptSubmit it rejects the prompt. On Stop it prevents Claude from stopping. On PostToolUse the tool has already run, so exit 2 only shows stderr to Claude. For richer control, print JSON instead; for PreToolUse, the decision goes in hookSpecificOutput.permissionDecision, not at the top level. A misplaced field parses fine and is silently ignored.

What are some Claude Code hooks you can copy?

Each example below uses a documented event and field. The first four follow patterns from the official hooks guide. Several use jq to read the JSON, so install it first (brew install jq on macOS, apt-get install jq on Debian and Ubuntu).

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

That is the macOS version. On Linux, swap the command for notify-send 'Claude Code' 'Claude Code needs your attention'. If nothing appears on macOS, Script Editor may lack notification permission; run osascript -e 'display notification "test"' once and enable it in System Settings, then Notifications.

Example 2: auto-format every edited file (PostToolUse, in .claude/settings.json)
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

Ask Claude to add a line with single-quoted strings to a JavaScript file; with Prettier's defaults the hook rewrites them to double quotes. A successful hook shows nothing in the conversation, so check the file.

Example 3: block edits to protected files (PreToolUse). Save as .claude/hooks/protect-files.sh
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

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
Example 3, continued: make it executable and register it (.claude/settings.json)
chmod +x .claude/hooks/protect-files.sh

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

Claude receives the Blocked: message as feedback and can pick another approach. Hook scripts must be executable on macOS and Linux. Referencing the script through $CLAUDE_PROJECT_DIR keeps the path valid from any working directory.

Example 4: re-inject key context after compaction (SessionStart with a compact matcher)
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Reminder: use pnpm, not npm. Run pnpm test before committing.'"
          }
        ]
      }
    ]
  }
}

Whatever the command prints to stdout is added to Claude's context. Replace the echo with something dynamic, such as git log --oneline -5, to show recent commits. For context you want in every session, use CLAUDE.md instead.

Example 5: stop force pushes (PreToolUse on Bash with an if filter). Save the script as .claude/hooks/no-force-push.sh
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command' <<<"$(cat)")
if [[ "$COMMAND" == *"--force"* || "$COMMAND" == *" -f"* ]]; then
  echo "Blocked: force pushes are not allowed in this project" >&2
  exit 2
fi
exit 0

# .claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git push *)",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/no-force-push.sh"
          }
        ]
      }
    ]
  }
}

The if filter means the script runs only for git push commands, not for every Bash call. The script follows the docs' own rm example: read the command from tool_input.command, exit 2 with a message on stderr to block, and exit 0 to leave the normal permission flow alone. Note that the string match is blunt: it also blocks --force-with-lease. Loosen the pattern if your team uses that.

Example 6: ask a model whether the work is finished (prompt hook on Stop, from the docs)
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Evaluate if Claude should stop: $ARGUMENTS. Check if all tasks are complete."
          }
        ]
      }
    ]
  }
}

A prompt hook sends the event JSON to a Claude model, which returns a decision. It adds a model call each time it fires, so use it sparingly. If you write your own Stop hook in a script, check the stop_hook_active field and exit 0 when it is true; otherwise a hook that keeps blocking is overridden after eight consecutive continuations.

How do you test and debug a hook?

  1. Check it registered. Type /hooks. Your hook should appear under its event. If it does not, validate the JSON: trailing commas and comments are not allowed. A restart forces a reload if the file watcher missed your edit.
  2. Test the script by hand. Pipe sample JSON in and read the exit code: echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh; echo $?.
  3. Read the outcome. Press Ctrl+O for the transcript view. A blocking error shows its message; a non-blocking one shows a hook error notice. For full detail, start with claude --debug-file /tmp/claude.log and tail the file.
  4. Fix the usual suspects. jq: command not found means install jq. A script that never runs probably needs chmod +x. A matcher that never matches is usually a case error.
  5. Check your shell profile. If your hook prints JSON but nothing happens, an unconditional echo in ~/.zshrc or ~/.bashrc may be printing before the JSON. Guard it with a check for an interactive shell.

A hook runs with your user's permissions, so treat it like any script. Read hooks in a repository you clone before you run Claude Code there, because a project's .claude/settings.json can define them. If you are experimenting, the Claude Code tutorial is a safe place to start, and the permissions guide explains the rules that sit beside hooks.

Hooks pay off most in repetitive workflows, such as changelog generation, test gating and release checks. If you want a project to practice on, browse the developer tools ideas on the board and pick one you can finish.

Frequently asked questions

What are Claude Code hooks?

Hooks are handlers that Claude Code runs automatically at specific points in a session, such as before a tool call, after a file edit, or when Claude finishes responding. They can be shell commands, HTTP requests, MCP tool calls, prompts or agents. You define them in a settings.json file.

Where do I configure Claude Code hooks?

Add a hooks block to a settings file. Use ~/.claude/settings.json for all your projects, .claude/settings.json for a project you want to share through git, or .claude/settings.local.json for personal project settings. Plugins and skills can also carry their own hooks.

How do I block an action with a hook?

Use a PreToolUse hook that reads the tool call from stdin and exits with code 2, writing the reason to stderr. Claude Code blocks the tool call and passes your message back to Claude. Exit 0 lets the normal permission flow continue. A denial holds even in bypassPermissions mode.

What is the difference between a hook and CLAUDE.md?

CLAUDE.md is context Claude tries to follow, so compliance is not guaranteed. A hook is code that runs every time its event fires, whatever Claude decides. Use CLAUDE.md for conventions and preferences, and hooks for rules that must always hold, like formatting or protecting files.

Why is my Claude Code hook not firing?

Run /hooks and confirm it appears under the right event. Check the matcher, which is case-sensitive and must match the tool name exactly. Make sure the JSON is valid, the script is executable, and you picked the right event: PreToolUse runs before a tool, PostToolUse after.

Can hooks undo what Claude already did?

No. A PostToolUse hook runs after the tool has finished, so it cannot undo the action. To prevent a change, use a PreToolUse hook. To reverse one, use /rewind or git. PostToolUse hooks can still show Claude an error message so it can correct itself.

Created with AI assistance and edited for this site. Examples are illustrative and do not represent customer research. About the publisher.