What Are Claude Code Hooks | Complete Event List, Configuration & Automation Guide
Claude Code Hooks are user-defined commands that run automatically at specific points in the session lifecycle. Automatic formatting after file edits, instant blocking of dangerous commands, desktop notifications when work completes — these can all be achieved deterministically, without relying on the model's "judgment." This article, based on Anthropic's official documentation (Hooks Reference and Automating with Hooks Guide), explains everything from the complete event list to configuration methods and practical use cases.
Hooks provide nearly 30 types of events including PreToolUse, PostToolUse, SessionStart, and Stop, with handlers available in 5 types: command, http, prompt, agent, and more. Using matchers, you can trigger hooks only for specific tools or argument patterns.
Configuration is written in JSON in either ~/.claude/settings.json (shared across all projects) or .claude/settings.json (project-specific). This article, based on Anthropic's official documentation, covers everything from the complete event list to configuration methods and practical use cases.
目次 (13)
- The Problem Hooks Solve
- Complete Event List
- The 5 Handler Types
- Configuration Files and Scope
- Narrowing Trigger Targets with Matchers
- Practical Use Cases
- Receiving Desktop Notifications When Work Completes
- Running Prettier Automatically After Edits
- Blocking Writes to Sensitive Files
- Re-injecting Context After Compaction
- Auto-reloading Environment Variables with direnv
- Exit Codes and JSON Output
- Debugging and Caveats
The Problem Hooks Solve
Claude Code writes code and executes commands as instructed, but enforcing rules such as "always run Prettier after every edit" or "unconditionally reject writes to .env" depends on the model's discretion. If the model decides "this isn't necessary right now," it won't be executed.
Hooks solve this problem. Every time a specific event fires, a shell command or HTTP endpoint is invoked and the action executes deterministically, without going through the LLM's judgment. Anthropic officially states that "Hooks guarantee that certain actions always occur."
Complete Event List
Hooks are categorized by when they fire. The most common ones are shown below.
| Event | When It Fires | Primary Use |
|---|---|---|
SessionStart |
When a session starts or resumes | Environment setup, context injection |
UserPromptSubmit |
When a prompt is submitted | Prompt validation, adding extra context |
PreToolUse |
Before a tool executes | Blocking or approving tool calls |
PostToolUse |
After a tool executes | Lint/formatting, logging |
PermissionRequest |
When a permission dialog appears | Auto-approve or auto-deny |
Stop |
When Claude completes a response | Task completion check, notifications |
Notification |
When Claude is waiting for input | Desktop notifications |
SessionEnd |
When a session ends | Cleanup processing |
ConfigChange |
When a config file is modified | Audit logging |
CwdChanged |
When the working directory changes | direnv integration |
FileChanged |
When a watched file is modified | Environment reload |
PreCompact / PostCompact |
Before and after context compaction | Context re-injection |
The official complete list defines nearly 30 events including SubagentStart, TaskCreated, and WorktreeCreate.
The 5 Handler Types
Hooks can be executed in 5 different ways.
- command (most common): Executes a shell command. JSON is passed via stdin
- http: POSTs to a specified URL. Can delegate to cloud functions or internal services
- mcp_tool: Calls a tool on a connected MCP server
- prompt: Has an LLM (Haiku by default) make a judgment. Useful for conditional logic that doesn't fit into rules
- agent: Launches a sub-execution environment with file reading and search tools. Used for complex validation
command and http are deterministic; prompt and agent are judgment-based via an LLM.
Configuration Files and Scope
Hooks are written in JSON and placed in the appropriate file based on scope.
| File Path | Scope | Committable to Repository |
|---|---|---|
~/.claude/settings.json |
Shared across all projects | No (machine-local) |
.claude/settings.json |
Single project | Yes |
.claude/settings.local.json |
Single project | No (gitignored) |
The basic structure has 3 layers.
{
"hooks": {
"PostToolUse": [ // ① Event name
{
"matcher": "Edit|Write", // ② Matcher
"hooks": [ // ③ Handler
{
"type": "command",
"command": "..."
}
]
}
]
}
}
After configuration, you can type /hooks in the CLI to view the list of registered hooks.
Narrowing Trigger Targets with Matchers
Using the matcher field, you can restrict a hook to specific tools or events rather than all invocations.
""→ Specifying an empty string matches all events (no filter)"Bash"→ Only Bash tool calls"Edit|Write"→ Only Edit or Write (pipe for OR logic)"mcp__github__.*"→ All tools on the GitHub MCP server (regex)"compact"→ Only during compact resume within SessionStart
You can also use the if field to filter by argument content in addition to tool name (Claude Code v2.1.85 and later).
{
"if": "Bash(git *)",
"command": "..."
}
This ensures the hook process only starts for git commands, preventing unnecessary process creation for unrelated Bash calls.
Practical Use Cases
Receiving Desktop Notifications When Work Completes
When Claude is waiting for input, you can receive notifications without having to watch the terminal.
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' 'Claude Code needs your attention'"
}
]
}
]
}
}
Specifying an empty string for matcher matches all events. Use "" when no filtering is needed.
On macOS, use osascript -e 'display notification "..."'; on Windows (PowerShell), use the MessageBox API.
Running Prettier Automatically After Edits
Set a PostToolUse hook with the Edit|Write matcher, extract the edited file path with jq, and pass it to Prettier.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
No matter how many files Claude edits, you'll never need to run Prettier manually again.
Blocking Writes to Sensitive Files
Instantly reject edits to protected files like .env or package-lock.json using exit 2.
#!/bin/bash
# .claude/hooks/protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done
exit 0
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
Returning exit 2 causes Claude to receive the reason for the block and try a different approach.
Re-injecting Context After Compaction
After context compaction, you can re-inform Claude of important conventions.
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: Use Bun, run bun test before committing'"
}
]
}
]
}
}
Text written to stdout is added to Claude's context.
Auto-reloading Environment Variables with direnv
When changing directories, reload environment variables with direnv and have them take effect immediately in the Bash tool.
{
"hooks": {
"CwdChanged": [
{
"hooks": [
{
"type": "command",
"command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
}
]
}
]
}
}
Content written to CLAUDE_ENV_FILE is automatically applied as a preamble when subsequent Bash commands are executed.
Exit Codes and JSON Output
Hook behavior is controlled by shell exit codes.
| Exit Code | Behavior |
|---|---|
0 |
No objection. Normal permission flow continues |
2 |
Blocks the action. stderr content becomes feedback to Claude |
| Other | Non-blocking error. Execution continues but a warning appears in the transcript |
Instead of exit 2 with an error message, you can also use structured output by writing JSON to stdout and returning exit 0.
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Please use rg (ripgrep) instead of grep"
}
}
permissionDecision can be "allow", "deny", or "ask". "allow" skips the interactive permission prompt, but cannot override deny rules in the configuration file.
Debugging and Caveats
Here are the checkpoints when a hook isn't working.
- Verify the hook appears under its event using the
/hookscommand - Check that the script has execute permissions (
chmod +x) - Use
claude --debug-file /tmp/claude.logto output debug logs and check match status, exit codes, and stderr - If your shell profile (
~/.bashrc, etc.) contains unconditionalechostatements, stdout will be polluted and JSON parsing errors will occur. Guard withif [[ $- == *i* ]]; thenso it only runs in interactive shells - If a
Stophook blocks 8 consecutive times, it gets overridden. To prevent infinite loops, checkstop_hook_activein the hook input and returnexit 0early
Hooks are powerful automation tools, but note that returning "allow" in PreToolUse cannot bypass deny rules in management policies. Conversely, "deny" can enforce blocking even in bypassPermissions mode, making it useful for policy enforcement.
For detailed input/output schemas and asynchronous hooks, refer to Anthropic's official Hooks Reference and Hooks Guide.