On this page
  1. When should you use a hook instead of CLAUDE.md or a skill?
  2. Which hook events matter most?
  3. How does a hook work?
  4. How do you return a structured decision?
  5. Hook 1: format every file Claude edits
  6. Hook 2: block edits to protected files
  7. Hook 3: get a desktop alert when Claude needs you
  8. Hook 4: re-inject key rules after compaction
  9. Which hook types are there besides shell commands?
  10. How do hooks interact with permission modes?
  11. Where do hooks live?
  12. What should you know about hook security?
  13. How do you debug a hook?
  14. Summary
  15. References

Hooks are your own shell commands, and optionally HTTP endpoints, MCP tool calls, prompts or subagents, that Claude Code runs automatically at fixed points in its lifecycle: when a session starts, before a tool runs, after an edit, when Claude stops, and at more than 30 lifecycle events in all. Unlike instructions in CLAUDE.md, which Claude usually follows, hooks always run. You configure them in a settings file as event, matcher and handler; a hook that exits with code 2 blocks the action and tells Claude why.

Anthropic’s guidance is blunt: “Use hooks for actions that must happen every time with zero exceptions.” If you’ve ever told Claude “don’t touch the .env file” and hoped for the best, a hook is the fix.

When should you use a hook instead of CLAUDE.md or a skill?

HookCLAUDE.md or skill
RunsYour handler, every time its event firesInstructions Claude reads and interprets
Guaranteed?Yes, the trigger is deterministicUsually followed, never enforced
Context costZero for command hooks, unless they return outputLoaded into context
Best forFormatting, blocking unsafe actions, logging, alertsConventions, knowledge and workflows that need judgment

Which hook events matter most?

EventFiresTypical use
SessionStartWhen a session begins, resumes, clears or compactsLoad context, such as today’s tickets
UserPromptSubmitWhen you send a prompt, before Claude sees itAdd context or block certain prompts
PreToolUseBefore a tool call runs; can block itProtect files, forbid dangerous commands
PermissionRequestWhen a tool call needs a permission decisionAuto-answer routine approvals
PostToolUseAfter a tool call succeedsFormat and lint edited files
NotificationWhen Claude Code sends a notificationDesktop alert when Claude needs you
StopWhen Claude finishes respondingRun tests and refuse to stop until they pass
PreCompact, PostCompactAround context compactionSave or restore state around a summary
FileChangedWhen a watched file changes on diskReload environment files on any change
SessionEndWhen a session terminatesLog or notify a team channel

The full list in the hooks guide also covers Setup for one-time CI preparation, PermissionDenied when auto mode refuses a call, PostToolBatch after parallel tool calls, InstructionsLoaded when a CLAUDE.md or rule file loads, subagents, tasks, worktrees, model switches and MCP elicitations.

Timeline of a Claude Code session showing where SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Notification and Stop hooks fire, with exit code meanings
Where the main hooks fire in a session, and what their exit codes mean.

How does a hook work?

Claude Code sends the hook a JSON description of the event on standard input: common fields such as session_id and cwd, plus event data such as tool_name and tool_input. The hook does its work and answers with its exit code:

  • Exit 0: no objection. For a PreToolUse hook, the normal permission flow still applies; for SessionStart and UserPromptSubmit, plain text printed to stdout is added to Claude’s context.
  • Exit 2: block the action. What you write to stderr becomes feedback, so Claude can adjust. A few events, such as SessionStart, can’t be blocked.
  • Anything else: usually a non-blocking error that the transcript reports as a hook error.

When several hooks match the same event, they all run in parallel and to completion. For PreToolUse the most restrictive answer wins, in the order deny, defer, ask, allow, but a deny from one hook doesn’t stop a sibling hook’s side effects, such as writing a log line. Many examples use jq to read the JSON, so install it first.

How do you return a structured decision?

Exit codes only block or stay silent. For more control, exit 0 and print JSON instead. This PreToolUse output denies a call and tells Claude what to do instead:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Use rg instead of grep for better performance"
  }
}

"ask" shows the normal prompt and "allow" skips it. To add context from a UserPromptSubmit hook, put additionalContext inside hookSpecificOutput; at the top level, Claude Code silently ignores it. PostToolUse and Stop hooks use a top-level "decision": "block" instead. Choose either exit codes or JSON for each hook, not both.

Hook 1: format every file Claude edits

Add this to .claude/settings.json to run Prettier after each edit or write:

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

Swap Prettier for your own formatter or linter, such as php-cs-fixer or ruff format. One gap to know: Claude can also change files through shell commands, which an Edit|Write matcher never sees. If every change must be caught, for compliance or audit logs, add a Stop hook that checks git status --porcelain once per turn, or a FileChanged hook for specific files.

Hook 2: block edits to protected files

Save this script as .claude/hooks/protect-files.sh and make it executable:

#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
FILE_PATH="${FILE_PATH//\\//}"   # normalise Windows separators

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

Then register it as a PreToolUse hook:

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

Ask Claude to add a comment to .env, and the edit is blocked before it happens, with the reason passed back to Claude.

Hook 3: get a desktop alert when Claude needs you

Put a Notification hook in ~/.claude/settings.json so you can work on something else while Claude runs. On macOS:

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

The matcher limits alerts to the moments Claude is waiting on you: a permission prompt or an idle prompt. On Linux, use notify-send; on Windows, a short PowerShell command that shows a toast or plays a sound does the same job.

Hook 4: re-inject key rules after compaction

Compaction summarises the conversation and can drop details. A SessionStart hook with the compact matcher puts critical context back every time:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Reminder: use Bun, not npm. Run bun test before committing.'; git log --oneline -5"
          }
        ]
      }
    ]
  }
}

For context every session needs, CLAUDE.md is still the right place; use this for dynamic state such as recent commits or the current sprint.

Which hook types are there besides shell commands?

TypeWhat it doesDefault timeout
commandRuns a shell command or script10 minutes
httpPOSTs the event JSON to a URL and reads the decision from the response body10 minutes
mcp_toolCalls a tool on a configured MCP server10 minutes
promptAsks a Claude model for a single-turn judgement30 seconds
agentSpawns a subagent that can read files and run commands before deciding (experimental)60 seconds

A prompt hook answers with "ok": true or "ok": false and a reason. On a Stop event, a false answer feeds the reason back so Claude keeps working:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
          }
        ]
      }
    ]
  }
}

Use a prompt hook when the event data alone is enough to decide, and an agent hook, which can take up to 50 tool-use turns, when the decision depends on the real state of the code, such as whether the test suite passes. HTTP hooks suit a shared audit service for a whole team; a non-2xx response is a non-blocking error unless you set onFailure: "block", and environment variables in headers resolve only if listed in allowedEnvVars.

How do hooks interact with permission modes?

PreToolUse hooks fire before any permission-mode check, in every mode. A hook that returns deny blocks the tool even in bypassPermissions mode or with --dangerously-skip-permissions, which makes hooks the way to enforce a policy users can’t switch off by changing modes. The reverse doesn’t hold: a hook returning allow can’t override a deny rule from settings, and hooks in settings files and plugins can tighten restrictions but never loosen them past what your permission rules allow.

Where do hooks live?

LocationApplies to
~/.claude/settings.jsonAll your projects
.claude/settings.jsonOne project, shared through git
.claude/settings.local.jsonOne project, just you
Managed policy settingsThe whole organisation
Plugin hooks/hooks.jsonWherever the plugin is enabled
Skill or subagent frontmatterWhile that skill or subagent is active

Run /hooks to see every configured hook grouped by event. Hooks from all sources merge: every matching hook fires.

What should you know about hook security?

Hooks run with your user permissions. A repository’s .claude/settings.json can define hooks, and a non-interactive claude -p run executes them even in a folder you have never trusted, unless you start with --bare or pass --settings '{"disableAllHooks": true}'. Review the hooks in any repository or plugin before you run Claude Code on it.

How do you debug a hook?

  • Run /hooks to confirm it is registered under the right event.
  • Check the matcher: it is case-sensitive and matches tool names such as Bash or Edit|Write.
  • Test the script on its own: echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh; echo $?
  • Run claude --debug or /debug to see which hooks matched, their exit codes and their full output.
  • If a Stop hook keeps Claude going forever, Claude Code overrides it after eight blocks in a row; check the stop_hook_active field and exit early when it is true.
  • Make sure nothing in your shell profile prints text before a hook’s JSON output.

Our free Claude Code hooks generator writes these settings.json blocks for you: pick the event and the command, then copy the result.

Summary

Hooks turn “please always” into “always”: formatting after every edit, blocks on protected files, alerts when Claude needs you, rules restored after compaction, tests before Claude stops. Use exit code 2 or JSON decisions for control, prompt and agent hooks when a decision needs judgment, and remember that a PreToolUse deny beats every permission mode. For the instructions side of the same coin, see our CLAUDE.md guide, and for packaging hooks with other components, our guide to Claude Code plugins.

References