Claude Code Hooks

Complete Beginner-Friendly Chapter • Automate Actions at Claude Code Lifecycle Events
BIG IDEA

Hooks make certain actions happen automatically and deterministically.

A Claude Code hook is a user-defined handler that runs when a specific lifecycle event occurs. The most common hook is a shell command, but current Claude Code also supports HTTP, MCP-tool, prompt-based and agent-based hooks. Hooks are useful for formatting files, enforcing rules, sending notifications, injecting context, auditing changes and controlling selected tool calls.

1. What Is a Hook?

Claude Code has a lifecycle: a session starts, you submit prompts, Claude calls tools, tools finish, Claude responds, the session ends, and so on. A hook lets you attach your own behavior to one of those moments.

Claude Code Event Hook fires Command / HTTP / MCP / Model Result
Simple mental model:
A Skill tells Claude how to do something.
A Hook says “whenever this event happens, automatically run this behavior.”

Why are hooks useful?

⚙ Automation
Run repetitive commands automatically.
🛡 Guardrails
Block dangerous or protected operations.
✨ Formatting
Format files immediately after edits.
🔔 Notifications
Know when Claude needs your attention.
🧠 Context
Re-inject important information after compaction.
📋 Audit
Log configuration or tool activity.

2. Create Your First Hook

The easiest starting point is a Notification hook. It can notify you when Claude needs your input instead of requiring you to watch the terminal.

Step 1 — Open settings

For a user-wide hook:

~/.claude/settings.json

Step 2 — Add a Notification hook

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

The example above is for macOS. Linux and Windows use their platform-specific notification commands.

Step 3 — Verify

/hooks

The /hooks browser lists hook events and shows configured hooks, including the event, matcher, type, source file and command.

The /hooks browser is read-only. Add, change or remove hooks by editing the settings JSON or asking Claude to make the change.

Step 4 — Test

Return to the CLI, switch to manual permission mode, ask Claude to do something that requires permission, and switch away from the terminal. The desktop notification should appear.

3. Hook Events

Hooks fire at specific points in Claude Code's lifecycle. Current Claude Code exposes many events. The most important ones for beginners are:

EventWhen it firesTypical use
SessionStartSession begins or resumesLoad environment/context.
UserPromptSubmitUser submits a promptValidate or add context.
PreToolUseBefore a tool call executesBlock/approve/check a tool call.
PermissionRequestA tool call needs permissionAutomatically allow selected requests.
PostToolUseAfter a tool call succeedsFormat or log changes.
PostToolUseFailureAfter a tool call failsCapture failures.
PostToolBatchAfter a parallel tool batch resolvesProcess a completed batch.
NotificationClaude Code sends a notificationDesktop notifications.
StopClaude finishes respondingValidate final state.
SubagentStartA subagent is spawnedTrack specialized workers.
SubagentStopA subagent finishesCollect/log completion.
TaskCreatedA task is createdTrack task lifecycle.
TaskCompletedA task is marked completeRun completion logic.
ConfigChangeConfiguration changesAudit or block changes.
CwdChangedWorking directory changesReload environment.
FileChangedA watched file changesReact to specific files.
WorktreeCreateA worktree is createdCustom worktree setup.
WorktreeRemoveA worktree is removedCleanup.
PreCompactBefore compactionPrepare/save context.
PostCompactAfter compactionRestore context.
PreModelSwitchBefore model switchAllow/block a switch.
PostModelSwitchAfter model changesReact to model change.
ElicitationMCP asks user for inputObserve/intercept elicitation.
ElicitationResultUser responds to MCP elicitationReact before response goes back.
SessionEndSession terminatesCleanup.
Most important beginner events:
PreToolUse = before
PostToolUse = after
SessionStart = beginning
Stop = Claude finished
Notification = notify me

4. Hook Types

Most examples use shell commands, but current Claude Code supports five hook handler types.

TypeMeaningBest for
commandRun a shell command.Scripts, formatting, validation, logging.
httpPOST event data to a URL.External services/webhooks.
mcp_toolCall a tool on an already-connected MCP server.Use existing MCP integrations.
promptAsk a Claude model to evaluate a condition in a single turn.Judgment-based checks.
agentUse a multi-turn agent with tool access for verification.More complex verification; experimental.
Deterministic vs judgment:
Use a command hook when the rule is exact: “Never edit .env.”
Use a prompt/agent hook when the question needs reasoning: “Does this change introduce a security problem?”

5. Hook Input

When an event fires, Claude Code passes event-specific JSON to a command hook through stdin.

Common fields include:

  • session_id — unique session identifier.
  • cwd — working directory when the event fired.
  • hook_event_name — event that triggered the hook.
  • tool_name — tool Claude is using for tool-related events.
  • tool_input — arguments passed to the tool.

Example: PreToolUse input

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

A Bash hook can therefore inspect exactly what Claude is about to execute.

Read the input in Bash

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

echo "Claude wants to run: $COMMAND"
Easy formula:
Event → JSON on stdin → your script reads JSON → script decides what to do → output/exit code.

6. Hook Output and Exit Codes

A command hook communicates with Claude Code through stdin, stdout, stderr and exit codes.

Exit codeMeaning
0No objection through the exit code. For PreToolUse, this does not itself approve the call; normal permission handling still applies.
2Blocks the action where that event supports blocking. Write the reason to stderr.
Other non-zeroBehavior depends on whether the hook emitted valid structured JSON or plain text and on the event.

Block a dangerous Bash command

#!/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

When Claude tries to execute a command containing drop table, the hook blocks the tool call and provides the stderr message as feedback.

Do not confuse exit 0 with “approve.” For a PreToolUse hook, exit 0 means the hook itself has no objection; the normal permission system still applies.

7. Structured JSON Output

Exit codes are useful for simple behavior. Structured JSON gives hooks more precise control. For supported events, print a JSON object to stdout and exit successfully.

PreToolUse deny

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Use rg instead of grep for better performance"
  }
}
DecisionEffect
allowSkip the interactive permission prompt, subject to other permission/organization rules.
denyCancel the tool call and send the reason to Claude.
askShow the permission prompt normally.
deferAvailable in non-interactive -p mode for external handling.

Inject context on UserPromptSubmit

{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "Current branch: release-42. Deploy freeze until Friday."
  }
}
For UserPromptSubmit, additionalContext belongs inside hookSpecificOutput. Putting it at the top level is ignored.

8. Matchers — Run Only When Needed

A matcher narrows a hook to specific event conditions. Without a matcher, a hook runs every time that event occurs.

Example: format only Edit and Write

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

The hook runs after Claude's Edit or Write tool, but not after unrelated tools.

Current matcher examples

EventMatcher filtersExamples
Tool eventsTool nameBash, Edit|Write, mcp__.*
SessionStartSession sourcestartup, resume, compact
NotificationNotification typepermission_prompt, idle_prompt
SubagentStart/StopAgent typegeneral-purpose, Explore
ConfigChangeConfiguration sourceuser_settings, project_settings
FileChangedLiteral filename.envrc|.env
SessionEndEnd reasonclear, resume

Since Claude Code v2.1.191, comma-separated alternatives also work in the same way as pipe alternation for supported matchers, for example "Edit, Write".

9. The if Field — Filter by Tool AND Arguments

The matcher filters at the hook-group level by tool name. The if field can go further by using permission-rule syntax against both the tool and its arguments.

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

This targets Git-related Bash commands rather than every Bash call.

PatternExampleMeaning
Bash(git *)git pushMatches Git commands.
Edit(*.ts)TypeScript editMatches edits to TypeScript files.
Bash(git push *)git push origin mainTargets push commands.
The if filter is best-effort when shell syntax is difficult to analyze. For hard security allow/deny rules, use the permission system rather than depending on a hook's if filter alone.

10. Where Should Hooks Live?

LocationScopeShareable?
~/.claude/settings.jsonAll your projectsNo — local machine configuration.
.claude/settings.jsonOne projectYes — can be committed.
.claude/settings.local.jsonOne projectNo — local/gitignored.
Managed policy settingsOrganization-wideYes — admin controlled.
Plugin hooks/hooks.jsonWhen plugin is enabledYes — bundled with plugin.
Skill frontmatterRest of session after Skill invocationYes — defined in Skill.
Subagent frontmatterWhile subagent runsYes — defined in subagent.

Which one should you choose?

Personal
Use ~/.claude/settings.json.
Project team
Use .claude/settings.json.
Personal project override
Use .claude/settings.local.json.
Reusable package
Use a plugin's hooks/hooks.json.
Skill-specific
Put hooks in Skill frontmatter when they should apply after invocation.
Specialized agent
Use subagent frontmatter for agent-lifetime hooks.

11. Real-World Automation Examples

A. Desktop Notification

Useful when Claude takes time and you want to work elsewhere.

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "notify-send 'Claude Code' 'Claude Code needs your attention'"
          }
        ]
      }
    ]
  }
}

B. Auto-format after edits

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

The current guide recommends jq for parsing JSON in these Bash examples.

C. Protect sensitive files

Create:

.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:

chmod +x .claude/hooks/protect-files.sh

Register it:

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

D. Re-inject context after compaction

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

The hook's stdout is added as plain text to Claude's context for supported session-start behavior.

E. Audit configuration changes

{
  "hooks": {
    "ConfigChange": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "jq -c '{timestamp: now | todate, source: .source, file: .file_path}' >> ~/claude-config-audit.log"
          }
        ]
      }
    ]
  }
}

F. Reload environment after directory changes

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
          }
        ]
      }
    ],
    "CwdChanged": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
          }
        ]
      }
    ]
  }
}

This pattern is useful when environment variables depend on the directory Claude is working in.

G. Auto-approve one narrow permission

{
  "hooks": {
    "PermissionRequest": [
      {
        "matcher": "ExitPlanMode",
        "hooks": [
          {
            "type": "command",
            "command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
          }
        ]
      }
    ]
  }
}
Security rule: Keep PermissionRequest matchers extremely narrow. An empty matcher or .* can accidentally auto-approve many tool permission prompts, including writes and shell commands.

12. Prompt-Based Hooks

Not every rule is deterministic. Sometimes the hook needs a model to evaluate a condition. Prompt-based hooks perform a single-turn LLM evaluation.

Event Prompt hook Claude evaluates Decision

Conceptually, use a prompt hook for questions like:

Does this change introduce a security vulnerability?
Return a block decision only if the evidence is strong.
Command hookPrompt hook
Deterministic script.Model evaluates context.
Good for exact rules.Good for judgment.
Fast/predictable.Uses model reasoning and can be less deterministic.

13. Agent-Based Hooks

Agent hooks are designed for more involved verification where the evaluator needs multiple turns and tool access.

Think of it as: a normal hook runs a command; a prompt hook asks a model one question; an agent hook gives a model more room and tools to investigate.

The current guide marks agent-based hooks as experimental, so behavior may change. Use them when the extra verification capability is worth that tradeoff.

14. HTTP Hooks

An HTTP hook sends event data to a URL with an HTTP POST request. This is useful when your automation lives outside the local machine.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "http",
            "url": "https://example.com/claude-hook"
          }
        ]
      }
    ]
  }
}

Typical architecture:

Claude Code Hook event HTTP POST Your service
Treat the receiving endpoint as trusted infrastructure. Hook payloads can contain useful session, tool and project context, so apply appropriate authentication and data-handling controls.

15. MCP Tool Hooks

An mcp_tool hook calls a tool on an already-connected MCP server. This lets hook automation use existing MCP capabilities rather than creating another shell script.

For tool matchers, MCP tools follow the naming pattern:

mcp__<server>__<tool>

For example:

mcp__github__search_repositories
mcp__filesystem__read_file

Plugin-bundled MCP servers can have a scoped server segment, such as:

mcp__plugin_my-plugin_db__query

16. Multiple Hooks Run Together

When multiple hooks match the same event, Claude Code runs matching hooks in parallel and waits for them before merging the results.

One hook returning deny does not stop sibling hooks from executing.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r .tool_input.command >> ~/.claude/bash.log"
          },
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh"
          }
        ]
      }
    ]
  }
}

The first hook logs the command. The second can block dangerous commands. Even if the second denies the call, the logging hook still ran.

PreToolUse decision precedence

deny→ stronger defer ask allow

For PreToolUse permission decisions, the most restrictive applicable result wins.

17. Hooks and Security

Hooks can run automatically and can block or allow operations. That makes them powerful. Treat hook configuration as code.

Security best practices

  • Keep matchers narrow.
  • Do not auto-approve every permission prompt.
  • Review shell scripts before committing them.
  • Quote paths and shell arguments carefully.
  • Do not expose secrets in logs.
  • Validate HTTP endpoints and authentication.
  • Use the permission system for hard allow/deny rules.
  • Test destructive cases before enabling a hook for the team.
  • Be careful when installing plugins containing hooks.
A hook runs because an event happened, not because Claude decided it would be useful. This is exactly why hooks are good guardrails — and why a badly written hook can have unintended effects.

18. Important Limitations

SituationWhat to remember
Hook modifies a fileBe aware of recursive triggers and unintended side effects.
Shell command is complexThe if matcher is best-effort; don't treat it as a security boundary.
Hook needs judgmentConsider prompt/agent hooks.
Hook needs external serviceUse HTTP or MCP when appropriate.
Hook is project-specificPrefer project settings.
Hook should be reusablePackage it in a plugin.
Hook output is malformed JSONClaude Code can report a non-blocking hook error.
Executable script on macOS/LinuxMake the script executable with chmod +x.

19. Troubleshooting

Problem: Hook does not fire

  1. Run /hooks and confirm it is registered.
  2. Check the event name.
  3. Check the matcher.
  4. Check the hook's source settings file.
  5. Run the command manually.
  6. Check executable permissions.
  7. Use claude --debug or /debug.

Problem: Formatter does not run

  • Confirm jq is installed.
  • Confirm Prettier is available.
  • Check that the event is PostToolUse.
  • Check that matcher includes Edit|Write.
  • Confirm the JSON path .tool_input.file_path exists for the event.

Problem: Protected-file hook does not block

  • Check chmod +x.
  • Run the script directly with sample JSON.
  • Verify the matcher is Edit|Write.
  • Verify the script exits with 2.
  • Check stderr output.

Problem: Hook says “no hooks configured”

Check the settings file location and whether the hook is valid JSON. Use the /hooks browser and debug logging to inspect configuration loading.

20. Spring Boot Example

Suppose your Spring Boot team wants Claude to run formatting and validation after Java edits. A project-level hook can automate part of that workflow.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); case \"$FILE\" in *.java) ./mvnw spotless:apply ;; esac"
          }
        ]
      }
    ]
  }
}

Conceptually:

Claude edits Java PostToolUse Check .java Spotless Formatted code
Use the exact formatter/build command that your project already trusts. The example demonstrates the hook pattern; it is not a requirement to use Spotless.

21. Next.js Example

A Next.js project can automatically run Prettier after Claude edits frontend files:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); case \"$FILE\" in *.ts|*.tsx|*.js|*.jsx|*.css) npx prettier --write \"$FILE\" ;; esac"
          }
        ]
      }
    ]
  }
}

22. Useful Hook Design Patterns

Guardrail
PreToolUse → inspect → block risky operation.
Formatter
PostToolUse → format changed file.
Notification
Notification → desktop alert.
Context loader
SessionStart → inject environment/project context.
Audit
ConfigChange / tool events → write an audit record.
Environment
CwdChanged → reload environment variables.
Cleanup
SessionEnd → remove temporary resources.
Model check
PreModelSwitch → allow or deny a model switch.
Agent lifecycle
SubagentStart/Stop → observe worker activity.

23. Hook vs Skill vs Plugin vs Permission

FeatureBest mental modelWhen to use
Hook“When X happens, automatically do Y.”Deterministic lifecycle automation.
Skill“Claude should know this workflow.”Reusable instructions and commands.
Plugin“Package my extensions.”Share Skills, agents, hooks, MCP/LSP and other capabilities.
Permission system“Is this operation allowed?”Hard access-control rules.
Prompt/Agent hook“Ask a model whether this is acceptable.”Judgment-based verification.
One-line memory:
Hook = event automation
Skill = reusable knowledge/workflow
Plugin = package of capabilities
Permission = access control

24. Recommended Workflow for Building Hooks

1. Pick event 2. Define exact rule 3. Add narrow matcher 4. Write script 5. Test manually 6. Test in Claude 7. Commit/share
  1. Choose the smallest lifecycle event that fits the requirement.
  2. Write down exactly what should happen.
  3. Use a narrow matcher or if filter.
  4. Keep the hook script small and testable.
  5. Test the script directly with representative JSON.
  6. Test it through Claude Code.
  7. Add logging only when useful.
  8. Move reusable hooks into a plugin when the team needs them.

25. Quick Cheat Sheet

GoalUse
Browse configured hooks/hooks
Debug hook problemsclaude --debug or /debug
Before a toolPreToolUse
After a successful toolPostToolUse
When Claude needs attentionNotification
Start/resumeSessionStart
After compactionSessionStart with compact matcher
Protect filesPreToolUse + Edit|Write
Format editsPostToolUse + Edit|Write
Audit settingsConfigChange
Reload env on directory changesCwdChanged
React to specific filesFileChanged
Clean up at session endSessionEnd
Call external endpointtype: "http"
Call connected MCP tooltype: "mcp_tool"
Use model judgmenttype: "prompt" or "agent"

26. Interview / Revision Questions

  1. What is a Claude Code hook?
  2. Why are hooks described as deterministic automation?
  3. What is the difference between a hook and a Skill?
  4. What happens during PreToolUse?
  5. What happens during PostToolUse?
  6. How does a command hook receive input?
  7. What does exit code 0 mean?
  8. What does exit code 2 mean?
  9. Why should PermissionRequest matchers be narrow?
  10. What is a matcher?
  11. What does the if field add beyond a matcher?
  12. Where can hooks be configured?
  13. How can a hook inject context after compaction?
  14. How can you protect .env from edits?
  15. What is the difference between command, prompt and agent hooks?
  16. What is an HTTP hook?
  17. What is an MCP tool hook?
  18. How are multiple matching hooks executed?
  19. Why shouldn't a hook's if filter be treated as a hard security boundary?
  20. When should a hook be packaged inside a plugin?

27. Practice Exercises

Exercise 1 — Notification

Create a Notification hook that alerts you when Claude needs input.

Exercise 2 — Formatter

Create a PostToolUse hook that formats JavaScript/TypeScript after Edit or Write.

Exercise 3 — Protected Files

Block edits to .env, .git/ and another sensitive file.

Exercise 4 — Context

Inject the current Git branch after session compaction.

Exercise 5 — Audit

Log every Bash command executed by Claude during a project session.

Exercise 6 — Permission

Auto-approve only one narrowly matched permission request and verify that unrelated requests still prompt.

Exercise 7 — HTTP

Send a harmless PostToolUse event to a local test endpoint and inspect the payload.

Exercise 8 — Plugin

Move your project's hook into a plugin's hooks/hooks.json and test it.

28. Final Memory Map

Event Matcher Hook Handler Input Decision / Side Effect Claude continues

Remember This

Hook = “When this event happens, automatically run this behavior.”

PreToolUse is your main guardrail point.

PostToolUse is your main formatting/logging point.

SessionStart is your main context/environment point.

Notification is your main “tell me Claude needs attention” point.

Prompt/agent hooks are for situations where simple deterministic scripts are not enough.

29. Final Takeaway

Claude Code hooks are one of the most useful ways to make an AI-assisted development workflow predictable. Instead of hoping Claude remembers to format a file, block a dangerous command, load an environment variable or notify you, a hook can make that behavior happen automatically at the right lifecycle event.

The strongest approach is to keep hooks small, narrow and deterministic. Use matchers to reduce unnecessary executions, use structured JSON when you need precise decisions, and use the permission system for hard security boundaries. When the automation becomes reusable across projects, package it as a plugin.