Claude Code Hooks — Automate Workflows with Deterministic Control
When you work with Claude Code, you sometimes need guaranteed behavior — formatting every edited file, blocking dangerous commands, or sending a notification whenever Claude waits for input. Relying on the LLM to remember these tasks is unreliable. Hooks solve this by letting you attach your own shell commands, HTTP calls, LLM prompts, or verification agents to specific lifecycle events inside Claude Code.
Think of hooks like event listeners in a web application: you register a handler for an event (e.g., PostToolUse), and Claude Code calls your handler every time that event fires — no prompting required.
What You Will Learn
- What hooks are and why they matter.
- The hook lifecycle — all 26 events you can listen to.
- Where to configure hooks (settings files, scopes).
- The four hook types: command, HTTP, prompt, agent.
- How hooks communicate: stdin, stdout, exit codes, and JSON.
- Practical, copy-paste examples for the most common use-cases.
- Filtering with matchers and the
iffield. - Troubleshooting and limitations.
What Are Hooks?
Definition
Hooks are user-defined actions that execute automatically at specific points in Claude Code's lifecycle. They provide deterministic control: certain actions always happen rather than relying on the LLM to choose to run them.
Why Hooks Matter
Without hooks, enforcing project rules or automating repetitive tasks depends on Claude remembering instructions in its context window. As context grows and compactions occur, instructions can get lost. Hooks guarantee execution:
- Consistency — auto-format every file edit, every time.
- Safety — block dangerous commands before they execute.
- Productivity — get notified when Claude needs input instead of watching the terminal.
- Compliance — audit every configuration change or tool call.
- Integration — connect Claude Code with external services, linters, and CI pipelines.
Four Hook Types
Claude Code supports four hook types, each suited to different scenarios:
command— runs a shell command. Input arrives on stdin as JSON; output goes to stdout/stderr.http— POSTs event data to a URL. The response body uses the same JSON format as command hooks.prompt— sends a single-turn prompt to a Claude model for a yes/no judgment call.agent— spawns a subagent that can read files, run commands, and make multi-turn decisions.
Hook Lifecycle — The 26 Events
Overview
Every hook is attached to an event. When that event fires, all matching hooks run in parallel. Below is the complete list grouped by category:
Session-Level Events
- SessionStart — when a session begins, resumes, or compacts. Supports
commandhooks only. - SessionEnd — when a session terminates (clear, resume, logout, etc.).
- InstructionsLoaded — when a CLAUDE.md or
.claude/rules/*.mdfile is loaded into context. - ConfigChange — when a configuration file changes during a session.
Turn-Level Events
- UserPromptSubmit — when you submit a prompt, before Claude processes it.
- Stop — when Claude finishes responding.
- StopFailure — when a turn ends due to an API error.
- Notification — when Claude Code sends a notification (permission prompt, idle, etc.).
Tool-Level Events
- PreToolUse — before a tool call executes. Can block it.
- PostToolUse — after a tool call succeeds.
- PostToolUseFailure — after a tool call fails.
- PermissionRequest — when a permission dialog would appear.
- PermissionDenied — when a tool call is denied.
Subagent & Task Events
- SubagentStart / SubagentStop — when a subagent is spawned or finishes.
- TaskCreated / TaskCompleted — when a task is created or completed.
- TeammateIdle — when an agent team teammate is about to go idle.
Environment & File Events
- CwdChanged — when Claude changes the working directory.
- FileChanged — when a watched file changes on disk.
- WorktreeCreate / WorktreeRemove — when a git worktree is created or removed.
Compaction & Elicitation Events
- PreCompact / PostCompact — before and after context compaction.
- Elicitation / ElicitationResult — when an MCP server requests user input or receives a response.
Where to Configure Hooks
Settings File Locations
Where you add a hook determines its scope. Claude Code checks multiple settings files, each serving a different audience:
| File | Scope | Shared with Team? |
|---|---|---|
~/.claude/settings.json | All your projects (user-level) | No — local to your machine |
.claude/settings.json | Single project | Yes — commit to the repo |
.claude/settings.local.json | Single project | No — gitignored |
| Managed policy settings | Organization-wide | Yes — admin-controlled |
Plugin hooks/hooks.json | When plugin is enabled | Yes — bundled with the plugin |
| Skill or agent frontmatter | While skill/agent is active | Yes — defined in the component file |
Configuration Structure — Three Nesting Levels
A hook configuration has three levels: event → matcher group → hook handler(s). Here is the JSON skeleton:
- Event name — the top-level key inside
"hooks"(e.g.,PreToolUse,Notification). - Matcher group — an array entry with a
"matcher"that filters when the group fires. - Hook handlers — the
"hooks"array inside each group, containing one or more actions to execute.
Multiple events live as sibling keys inside the single "hooks" object. If your settings file already has a "hooks" key, add new event names next to the existing ones rather than replacing the whole object.
Filtering with Matchers
How Matchers Work
Without a matcher, a hook fires on every occurrence of its event. A matcher narrows the trigger. Each event type matches on a specific field:
| Event(s) | Matches on | Example |
|---|---|---|
| PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied | Tool name | Bash, Edit|Write, mcp__.* |
| SessionStart | How the session started | startup, resume, compact |
| SessionEnd | Why the session ended | clear, logout |
| Notification | Notification type | permission_prompt, idle_prompt |
| ConfigChange | Configuration source | user_settings, project_settings |
| FileChanged | Literal filenames to watch | .envrc|.env |
| UserPromptSubmit, Stop, CwdChanged, etc. | No matcher support | Always fires on every occurrence |
Matcher Pattern Rules
"*"or empty string — matches everything.- Contains only letters, digits, underscores, and
|— treated as an exact name or pipe-separated list (e.g.,Edit|Write). - Contains other characters (dots, brackets, etc.) — treated as a JavaScript regular expression (e.g.,
mcp__.*).
The if Field — Filter by Tool Arguments
The if field (Claude Code v2.1.85+) goes beyond the group-level matcher. It uses permission rule syntax to filter by both tool name and arguments, so the hook process only spawns when the tool call matches.
This hooks fires only when the Bash tool runs a command starting with git. Other Bash commands skip it entirely. The if field only works on tool events: PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, and PermissionDenied.
How Hooks Communicate — Input, Output, and Exit Codes
Hook Input (stdin / POST body)
When an event fires, Claude Code sends event-specific JSON to your hook. Command hooks receive it on stdin; HTTP hooks receive it as a POST body. Every event includes common fields like session_id, cwd, and hook_event_name, plus event-specific data.
For example, a PreToolUse hook for a Bash command receives:
Exit Codes
The exit code tells Claude Code what to do next:
- Exit 0 — the action proceeds. For
UserPromptSubmitandSessionStarthooks, stdout text is added to Claude's context. - Exit 2 — the action is blocked. Write a reason to stderr; Claude receives it as feedback.
- Any other exit code — the action proceeds but a hook-error notice appears in the transcript.
Structured JSON Output
For more control than allow/block, exit 0 and print a JSON object to stdout. Different events use different JSON patterns:
PreToolUse — Permission Decisions
Available permissionDecision values for PreToolUse:
"allow"— skip the interactive permission prompt (deny/ask rules still apply)."deny"— cancel the tool call and send the reason to Claude."ask"— show the permission prompt to the user as normal."defer"— available in non-interactive mode (-p), preserves the call for an SDK wrapper.
PermissionRequest — Auto-Approve Decisions
PostToolUse / Stop — Block Decisions
These events use a top-level decision: "block" field. For Stop hooks, blocking tells Claude to keep working.
Practical Examples
Example 1: Get Notified When Claude Needs Input
Receive a desktop notification whenever Claude finishes working and waits for you, so you can switch to other tasks. This uses the Notification event.
macOS
Windows (PowerShell)
Add this to ~/.claude/settings.json. Verify with /hooks in the CLI.
Example 2: Auto-Format Code After Edits
Run Prettier on every file Claude edits. The PostToolUse event with an Edit|Write matcher ensures it runs only after file-editing tools.
Add this to .claude/settings.json in your project root. The command uses jq to extract the edited file path from the hook input JSON, then pipes it to Prettier.
Example 3: Block Edits to Protected Files
Prevent Claude from modifying sensitive files like .env, package-lock.json, or anything inside .git/. This uses a separate script file and a PreToolUse hook.
Step 1 — Create the Hook Script
Save this to .claude/hooks/protect-files.sh:
Step 2 — Make the Script Executable (macOS/Linux)
Step 3 — Register the Hook
Add a PreToolUse hook to .claude/settings.json:
When Claude tries to edit a protected file, the hook exits with code 2 and sends the reason to stderr. Claude receives this feedback and adjusts its approach.
Example 4: Block Dangerous Shell Commands
Prevent DROP TABLE statements from ever running:
Attach this to PreToolUse with "matcher": "Bash".
Example 5: Re-Inject Context After Compaction
When Claude's context window fills up, compaction summarizes the conversation, which can lose important details. A SessionStart hook with a compact matcher re-injects critical context. Any text your command writes to stdout is added to Claude's context.
You can replace the echo with any command that produces dynamic output, like git log --oneline -5 to show recent commits.
Example 6: Log Every Bash Command
Record each command Claude runs to an audit log:
Example 7: Audit Configuration Changes
Track when settings or skills files change during a session by appending each change to an audit log:
Example 8: Reload Environment on Directory Change
If you use direnv, a CwdChanged hook reloads environment variables whenever Claude changes directories:
Example 9: Auto-Approve a Specific Permission Prompt
Skip the approval dialog for ExitPlanMode so you aren't prompted every time a plan is ready. This uses a PermissionRequest hook with structured JSON output:
Warning: keep the matcher as narrow as possible. Matching on .* or leaving it empty would auto-approve every permission prompt, including file writes and shell commands.
Prompt-Based Hooks
When to Use Prompt Hooks
For decisions that require judgment rather than deterministic rules, use type: "prompt". Instead of running a shell command, Claude Code sends your prompt and the hook's input data to a Claude model (Haiku by default) to make the decision.
How It Works
The model returns a yes/no decision as JSON:
"ok": true— the action proceeds."ok": false+"reason"— the action is blocked, and the reason is fed back to Claude.
Example: Check Task Completeness Before Stopping
If the model returns "ok": false, Claude keeps working and uses the reason as its next instruction. You can specify a different model with the "model" field.
Agent-Based Hooks
When to Use Agent Hooks
When verification requires inspecting files or running commands, use type: "agent". Unlike prompt hooks (single LLM call), agent hooks spawn a subagent that can read files, search code, and use other tools to verify conditions.
Example: Verify Tests Pass Before Stopping
Agent hooks use the same "ok" / "reason" response format as prompt hooks, but with a longer default timeout (60 seconds) and up to 50 tool-use turns.
HTTP Hooks
When to Use HTTP Hooks
Use type: "http" when you want a web server, cloud function, or external service to handle hook logic — for example, a shared audit service that logs tool use events across a team.
Example: Post Tool Use to a Logging Service
Header values support environment variable interpolation ($VAR_NAME). Only variables listed in allowedEnvVars are resolved. The endpoint returns JSON in the same format as command hooks.
Building an HTTP Hook Endpoint in C# / ASP.NET Core
If you want to build your own hook receiver as a web API, here is a minimal ASP.NET Core endpoint that receives PostToolUse events and logs them:
For a blocking response (e.g., denying a PreToolUse call), return the appropriate JSON structure:
Combining Multiple Hooks
Multiple Events in One Settings File
Each event name is a key inside the single "hooks" object. Here is a settings file with several hooks working together:
Conflict Resolution
When multiple hooks match, each returns its own result. Claude Code picks the most restrictive answer. A PreToolUse hook returning "deny" cancels the tool call no matter what others return. One hook returning "ask" forces the permission prompt even if the rest return "allow".
Stop Hook Infinite Loop Prevention
The Problem
A Stop hook that always blocks will keep Claude working forever in an infinite loop.
The Fix
Check the stop_hook_active field from the JSON input and exit early if it's true:
Hooks and Permission Modes
PreToolUse hooks fire before any permission-mode check. A hook returning permissionDecision: "deny" blocks the tool even in bypassPermissions mode or with --dangerously-skip-permissions.
The reverse is not true: a hook returning "allow" does not bypass deny rules from settings. Hooks can tighten restrictions but not loosen them past what permission rules allow.
Debugging Hooks
The /hooks Menu
Type /hooks in Claude Code to browse all configured hooks grouped by event. A count appears next to each event that has hooks configured. Select an event to see hook details: event, matcher, type, source file, and command. The menu is read-only — to edit, modify the settings JSON directly.
Debug Logging
Start Claude Code with a debug file to capture full execution details:
Then in another terminal:
If you started without that flag, run /debug mid-session to enable logging.
Manual Hook Testing
Test your hook script by piping sample JSON:
Common Troubleshooting
- Hook not firing: run
/hooksto confirm it appears under the correct event. Check matcher case-sensitivity. - "command not found": use absolute paths or
$CLAUDE_PROJECT_DIR. - "jq: command not found": install jq or use Python/Node.js for JSON parsing.
- Script not running: make it executable with
chmod +x. - JSON validation failed: your shell profile may have unconditional
echostatements that pollute stdout. Wrap them in an interactive-shell check:
Limitations
- Command hooks communicate through stdout, stderr, and exit codes only. They cannot trigger
/commands or tool calls. - Hook timeout is 10 minutes by default, configurable per hook with the
timeoutfield (in seconds). PostToolUsehooks cannot undo actions — the tool has already executed.PermissionRequesthooks do not fire in non-interactive mode (-p). UsePreToolUsehooks for automated permission decisions.Stophooks fire whenever Claude finishes responding, not only at task completion. They do not fire on user interrupts. API errors fireStopFailureinstead.- When multiple
PreToolUsehooks returnupdatedInputto rewrite a tool's arguments, the last one to finish wins (hooks run in parallel, order is non-deterministic). SessionStartsupports onlycommandhooks.- On Windows, use
"shell": "powershell"for command hooks.
Quick Reference Card
| Hook Type | Best For | Key Fields |
|---|---|---|
command | Shell scripts, CLI tools, deterministic checks | command, shell, async |
http | External services, team-wide logging | url, headers, allowedEnvVars |
prompt | Judgment-based decisions, single-turn evaluation | prompt, model |
agent | Multi-turn verification, file inspection, running tests | prompt, model, timeout |
| Exit Code | Meaning | |
| 0 | Allow / success. Optionally return JSON on stdout for structured control. | |
| 2 | Block the action. Write a reason to stderr. | |
| Other | Action proceeds, but a hook-error notice appears in the transcript. |
Further Reading
- Hooks Reference — full event schemas, JSON output format, async hooks, MCP tool hooks.
- Hooks Guide — official quickstart and use-case walkthrough.
- Security Considerations — review before deploying hooks in shared or production environments.
- Bash Command Validator Example — complete reference implementation on GitHub.