Claude Code Hooks Explained

Claude Code Mastery
20 parts- 1Claude Code: The Future of Coding?
- 2What Is Claude Code? The Complete Guide for 2026
- 360 Claude Code Tips and Tricks for Power Users
- 4Claude Code Sub Agents: Parallel AI Development
- 5Claude Code Worktrees: Parallel Development Without the Chaos
- 6Building Multi-Agent Workflows in Claude Code: A Practical Tutorial
- 7The Ralph Loop: Running Claude Code For Hours Autonomously
- 8Self-Improving Skills: Claude Code That Learns From Every Session
- 9Claude Code Usage Limits in 2026: The Practical Playbook for Pro and Max Teams
- 10Progressive Disclosure: How Claude Code Cut Token Usage by 98%
- 11Claude Code + Chrome: AI Agents That Use Your Browser
- 12Interview Mode: Let Claude Code Ask the Questions First
- 13Claude Code: Remote Control, Auto Memory, Plugins & More
- 14Claude Code Hooks ExplainedCurrent
- 15How to Use Claude Code with Next.js
- 16Claude Code Channels: Telegram, Discord, iMessage, and Webhooks
- 17How to Migrate from GitHub Copilot to Claude Code
- 18Building a SaaS with Claude Code: End-to-End Guide
- 19Create Beautiful UI with Claude Code: The Style Guide Method
- 20Claude Code Loops: Recurring Prompts That Actually Run
TL;DR
Claude Code hooks run shell commands at exact lifecycle points. Set up auto-format, dangerous-command blocking, tests before stopping and desktop notifications.
Last updated: October 25, 2026. Re-verified against the current hooks reference: handler types, event list and default timeouts below now match it.
You can tell Claude Code "always run Prettier after editing files" in your CLAUDE.md. It will probably listen. Probably. But CLAUDE.md instructions are suggestions the model can choose to ignore. Hooks are not suggestions. They are shell commands that execute every single time, at exact points in Claude Code's lifecycle.
Think of hooks like git hooks, but for your AI coding agent. Before a tool runs, after a file gets edited, when the agent finishes responding, when a session starts. You define what happens at each point, and it happens deterministically. No forgetting. No deciding it's unnecessary this time.
For anyone running Claude Code on production codebases, the distinction between "probably follows the rule" and "always follows the rule" is everything.
Newer in October 2026: Claude Code mods are plugins whose JavaScript handlers run inside Claude Code and can draw panes and rewrite events, which settings hooks cannot. Both keep working side by side.
Official Sources#
| Source | What to verify |
|---|---|
| Claude Code hooks reference | Hook types, lifecycle events, exit codes, timeouts, and configuration schema |
| Claude Code settings | Settings file locations and precedence |
| Claude Code overview | Feature set, tool access, and platform support |
| Anthropic API reference | Sub-agent and prompt hook model routing |
What Are Claude Code Hooks?#
Hooks are shell commands, HTTP calls, MCP tool calls, LLM prompts, or sub-agents that Claude Code executes at specific lifecycle events. You configure them in JSON settings files, and they run automatically with zero manual intervention. These hooks run on each developer's machine; for an organization-wide, server-side check on every prompt across Claude products, see Claude Inference hooks.
For broader context, pair this with What Is Claude Code? The Complete Guide for 2026 and 60 Claude Code Tips and Tricks for Power Users; those companion pieces show where this fits in the wider AI developer workflow.
Every hook has three core parts:
- The event - when it fires (e.g.,
PostToolUse,PreToolUse,Stop) - The matcher - which tools trigger it (e.g.,
Write,Edit|Write,Bash) - The handler - what runs (a shell command, a prompt, or a sub-agent)
Here's the simplest possible hook. It runs Prettier every time Claude writes a file:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write $(cat | jq -r '.tool_input.file_path')"
}
]
}
]
}
}
That's it. Every Write operation now auto-formats. No reminders needed.
Hook Types#
Every hook has a type field that determines how it executes. The hooks reference lists five: command, http, mcp_tool, prompt and agent. This guide covers the three most common (command, prompt, agent); http hooks send the event to a URL and mcp_tool hooks call a tool on a connected MCP server, so check the reference for their fields.
Command Hooks#
The most common type. Runs a shell command as a child process. The command receives JSON context on stdin with the session ID, tool name, tool input, and working directory.
{
"type": "command",
"command": "npx prettier --write $(cat | jq -r '.tool_input.file_path')"
}
Use these for: auto-formatting, logging, notifications, file operations, blocking dangerous commands.
Prompt Hooks#
Sends a text prompt to a fast Claude model (Haiku by default) for single-turn evaluation. The $ARGUMENTS placeholder injects the hook's input JSON. No custom scripts needed.
{
"type": "prompt",
"prompt": "Analyze this context: $ARGUMENTS. Are all tasks complete and were tests run? Respond with {\"decision\": \"approve\"} or {\"decision\": \"block\", \"reason\": \"explanation\"}."
}
Use these for: context-aware decisions, task verification, intelligent filtering, without writing a custom script. For how other AI coding tools handle hooks, see the fail-open versus fail-closed comparison linked under Tips and Gotchas.
Agent Hooks#
Spawns a sub-agent with access to tools like Read, Grep, and Glob for multi-turn codebase verification. The heaviest handler type, but the most powerful.
Use these for: deep validation like confirming all modified files have test coverage, or checking that an API change updated all consumers.
Lifecycle Events#
Claude Code exposes lifecycle events that cover every stage of the agent's execution. Here are the ones you'll use most, plus the original core list. The current reference documents many more (over 30), including UserPromptExpansion, PermissionDenied, PostToolBatch, StopFailure, InstructionsLoaded, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, PreModelSwitch, Elicitation and TaskCreated.
The Big Four#
| Event | When It Fires | Use It For |
|---|---|---|
PreToolUse | Before Claude runs any tool | Block dangerous commands, protect files, validate inputs |
PostToolUse | After Claude runs any tool | Auto-format code, stage files, run linters, log actions |
Stop | When Claude finishes responding | Run tests, verify task completion, quality checks |
Notification | When Claude needs user attention | Desktop alerts, Slack messages, sound effects |
Core Event Reference#
| Event | When It Fires |
|---|---|
PreToolUse | Before any tool execution |
PostToolUse | After any tool execution |
PostToolUseFailure | After a tool execution fails |
Notification | When Claude sends an alert |
PermissionRequest | When a permission dialog would appear |
Stop | When Claude finishes its response |
SubagentStop | When a sub-agent finishes |
SubagentStart | When a sub-agent spawns |
PreCompact | Before context compaction |
PostCompact | After context compaction |
SessionStart | When a new session begins |
SessionEnd | When a session ends |
UserPromptSubmit | When you submit a prompt |
TaskCompleted | When a task completes |
Setup | During initialization |
PreToolUse, PostToolUse, Notification, and Stop handle 90% of real-world use cases.
Matchers#
Matchers filter which tools trigger a hook. They're regex strings matched against tool names.
| Matcher | What It Matches |
|---|---|
"Bash" | Shell commands only |
"Edit" | File edits only |
"Write" | File creation only |
"Edit|Write" | Any file modification |
"Bash|Edit|Write" | Most common operations |
"mcp__.*" | All MCP server tools |
"mcp__github__.*" | GitHub MCP tools only |
| Not specified | Everything |
Tool names are case-sensitive. "Bash" works. "bash" does not. "Edit" works. "edit" does not.
For Bash tool matchers, you can also match command arguments: "Bash(npm test.*)" matches any bash command starting with npm test.
Where Hooks Live#
Hooks are configured in JSON settings files at four levels:
| Scope | File Path | Use Case |
|---|---|---|
| Project | .claude/settings.json | Team-shared hooks, committed to git |
| Project local | .claude/settings.local.json | Personal project overrides, gitignored |
| User | ~/.claude/settings.json | Global hooks across all projects |
| Enterprise | Managed policy | Organization-wide enforcement |
Project-level hooks are the most common. Commit them to git so your whole team gets the same automation.
One important security detail: Claude Code snapshots your hook configuration at startup and uses that snapshot for the entire session. Edits mid-session have no effect. This prevents any modification of hooks while the agent is running.
Practical Examples#
1. Auto-Format on Save#
The highest-value hook for most projects. Run your formatter every time Claude edits or creates a file.
Prettier (JavaScript/TypeScript):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$FILE\" ] && npx prettier --write \"$FILE\" 2>/dev/null || true"
}
]
}
]
}
}
Go:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$FILE\" ] && [[ \"$FILE\" == *.go ]] && gofmt -w \"$FILE\" || true"
}
]
}
]
}
}
Python (Black):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$FILE\" ] && [[ \"$FILE\" == *.py ]] && python -m black \"$FILE\" 2>/dev/null || true"
}
]
}
]
}
}
The 2>/dev/null || true at the end is important. It prevents the hook from failing on files the formatter doesn't support.
2. Block Dangerous Commands#
Prevent Claude from running destructive shell commands, even in autonomous mode.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(cat | jq -r '.tool_input.command // empty') && if echo \"$CMD\" | grep -qEi '(rm\\s+-rf\\s+/|DROP\\s+TABLE|DROP\\s+DATABASE|mkfs\\.|:\\(\\)\\{|chmod\\s+-R\\s+777\\s+/|dd\\s+if=.*of=/dev/)'; then echo \"BLOCKED: Dangerous command detected\" >&2; exit 2; fi"
}
]
}
]
}
}
Exit code 2 is the key. It tells Claude Code to block the operation and feed the stderr message back to Claude as an error. Claude sees the message, understands why the operation was blocked, and adjusts its approach.
Blocked patterns include:
rm -rf /(recursive delete from root)DROP TABLE/DROP DATABASE(SQL destruction)mkfs.(format filesystem)- Fork bombs
chmod -R 777 /(recursive permission change on root)dd if=... of=/dev/(raw disk writes)
3. Protect Sensitive Files#
Block Claude from touching files that should never be AI-modified.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && if echo \"$FILE\" | grep -qE '(\\.env|\\.lock|secrets\\.yaml|credentials|id_rsa|\\.pem)'; then echo \"BLOCKED: Cannot modify protected file: $FILE\" >&2; exit 2; fi"
}
]
}
]
}
}
Customize the grep pattern for your project. Add migration files, CI configs, or anything else that shouldn't change without human review.
4. Desktop Notifications#
Get notified when Claude needs your attention or finishes a long task. Essential if you multitask while Claude works.
macOS:
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
Linux:
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' 'Claude needs your attention'"
}
]
}
]
}
}
Put this in ~/.claude/settings.json so it works across all projects.
5. Run Tests Before Stopping#
Force Claude to verify its own work before it finishes. This is the hook that changed how I use Claude Code.
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "npm test 2>&1 || (echo 'Tests are failing. Please fix before finishing.' >&2; exit 2)"
}
]
}
]
}
}
If tests fail, the Stop hook returns exit code 2, which forces Claude to continue working. Claude sees the test output and attempts to fix the failures. This creates an automatic test-fix loop.
For a smarter version, use a prompt hook that evaluates whether the task is actually complete:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Analyze this context: $ARGUMENTS. Were all requested tasks completed? Were tests run and passing? If not, respond with {\"decision\": \"block\", \"reason\": \"explanation\"}. If everything looks good, respond with {\"decision\": \"approve\"}."
}
]
}
]
}
}
6. Git Auto-Stage#
Automatically stage every file Claude modifies, so changes are always ready to commit.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$FILE\" ] && [ -f \"$FILE\" ] && git add \"$FILE\" 2>/dev/null || true"
}
]
}
]
}
}
Pair this with a solid .gitignore. You do not want to accidentally stage build artifacts or node_modules.
7. Inject Project Context on Session Start#
Load project-specific context automatically when Claude Code starts.
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo \"Current branch: $(git branch --show-current). Last 3 commits: $(git log --oneline -3). Open issues: $(gh issue list --limit 5 --json title -q '.[].title' 2>/dev/null || echo 'N/A')\""
}
]
}
]
}
}
The stdout from SessionStart hooks gets injected as context for Claude. Every session starts with awareness of your current branch, recent commits, and open issues. No more explaining where you left off.
8. ESLint Auto-Fix#
Run ESLint with auto-fix on JavaScript/TypeScript files after every edit.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$FILE\" ] && [[ \"$FILE\" =~ \\.(js|ts|jsx|tsx)$ ]] && npx eslint --fix \"$FILE\" 2>/dev/null || true"
}
]
}
]
}
}
The regex check prevents ESLint from running on files it can't handle.
Input and Output#
What Hooks Receive#
Hooks receive JSON on stdin with context about the current event. The structure varies by event type.
Base fields (all events):
{
"session_id": "abc123",
"transcript_path": "/Users/you/.claude/projects/my-project/conversation.jsonl",
"cwd": "/Users/you/my-project",
"hook_event_name": "PostToolUse"
}
Tool events add:
{
"tool_name": "Edit",
"tool_input": {
"file_path": "/Users/you/my-project/src/index.ts",
"old_string": "...",
"new_string": "..."
}
}
PostToolUse also includes tool_response with the result.
Use jq to extract specific fields in your hook commands:
# Get the file path
cat | jq -r '.tool_input.file_path'
# Get the bash command
cat | jq -r '.tool_input.command'
# Get the tool name
cat | jq -r '.tool_name'
Exit Codes#
For PreToolUse hooks, exit codes control flow:
| Exit Code | Effect |
|---|---|
0 | Allow the operation |
2 | Block the operation. stderr is sent to Claude as feedback |
| Other | Hook error. Operation proceeds, error is logged |
For PostToolUse hooks, the operation already happened, so the exit code doesn't block anything. But stderr output still gets sent to Claude as context.
Structured JSON Output#
PreToolUse hooks can return structured JSON on stdout for fine-grained control:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow"
}
}
Valid values for permissionDecision:
"allow"- skip the permission prompt, auto-approve"deny"- block the operation (same as exit code 2)"ask"- show the normal permission prompt to the user
This is useful for auto-allowing safe operations while still prompting for anything risky.
Setting Up Hooks#
Two ways to configure hooks.
Interactive: The /hooks Command#
Type /hooks in Claude Code. Choose the event, add a new hook, set your matcher, enter the command, save. Claude Code updates your settings file and reloads the configuration. This is the easiest way to get started.
Manual: Edit settings.json#
Open .claude/settings.json in your project (or ~/.claude/settings.json for global hooks) and add the hooks configuration directly.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "your-command-here"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "another-command-here"
}
]
}
]
}
}
Restart Claude Code or use /hooks to reload after manual edits.
Tips and Gotchas#
Keep hooks fast#
Every hook adds latency. A 200ms formatter is fine. A 30-second test suite on every file edit is not. Save heavy operations for Stop hooks, not PostToolUse.
Use || true to prevent cascading failures#
If your hook command fails on certain files (like running Prettier on a binary), the error can confuse Claude. Append || true to commands that might fail on edge cases.
Format on commit, not on every edit#
Auto-formatting on every PostToolUse works, but each format change triggers a system reminder to Claude about the file modification. This eats into your context window. For large projects, a better pattern is formatting on Stop or through a git pre-commit hook rather than on every individual edit.
Test hooks before deploying#
Ask Claude to write a test file and verify your hook triggers. Check Claude Code's transcript (Ctrl+O) for error messages if a hook doesn't seem to work.
Mid-session edits don't apply#
Claude Code snapshots hook configuration at startup. If you edit your settings.json while a session is running, the changes won't take effect until you start a new session.
Matcher regex is case-sensitive#
"Bash" matches. "bash" does not. Tool names are PascalCase: Bash, Edit, Write, Read, Glob, Grep.
Exit code 2 blocks, everything else doesn't#
Only exit code 2 blocks a PreToolUse operation. Exit code 1 or any other non-zero code is treated as a hook error and logged, but the operation still proceeds. Since v2.1.295 you can add "onFailure": "block" to a command or HTTP hook so a crash, timeout or missing script blocks instead. See which agent hooks fail open, and how to make them fail closed for test runs and how Codex, Cursor and Gemini CLI compare.
Hooks run in parallel when multiple match#
If you have three PostToolUse hooks with the same matcher, all three run simultaneously. They don't run sequentially.
Use the timeout field for slow commands#
The hooks reference gives default timeouts of 600 for command, http and mcp_tool hooks, 30 for prompt hooks and 60 for agent hooks (lower for some events, such as 30 on UserPromptSubmit and 10 on MessageDisplay). Those defaults are in seconds. Set timeout explicitly for anything slow, and keep it short for security guards. Example (120 seconds):
{
"type": "command",
"command": "npm test",
"timeout": 120
}
Don't block stdin in command hooks#
Your hook command receives JSON on stdin. If your command doesn't read stdin (like a simple echo), that's fine. But if it reads stdin and then hangs waiting for more input, the hook will timeout. Always consume stdin completely or ignore it.
Combining Hooks for a Full Workflow#
Here's a production-ready configuration that combines multiple hooks into a cohesive workflow:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo \"Branch: $(git branch --show-current). Last commit: $(git log --oneline -1). Node: $(node -v)\""
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(cat | jq -r '.tool_input.command // empty') && if echo \"$CMD\" | grep -qEi '(rm\\s+-rf\\s+/|DROP\\s+TABLE|DROP\\s+DATABASE)'; then echo \"BLOCKED: Dangerous command\" >&2; exit 2; fi"
}
]
},
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && if echo \"$FILE\" | grep -qE '(\\.env|\\.lock)'; then echo \"BLOCKED: Protected file\" >&2; exit 2; fi"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$FILE\" ] && npx prettier --write \"$FILE\" 2>/dev/null || true"
}
]
}
],
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude needs your attention\" with title \"Claude Code\"'"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "npm test 2>&1 | tail -20"
}
]
}
]
}
}
This gives you: project context on start, dangerous command blocking, sensitive file protection, auto-formatting, desktop notifications, and test output on completion. Six hooks covering the full development lifecycle.
Hooks vs. CLAUDE.md Rules#
| CLAUDE.md | Hooks | |
|---|---|---|
| Enforcement | Probabilistic (model may ignore) | Deterministic (always runs) |
| Speed | Zero overhead | Adds latency per hook |
| Flexibility | Natural language, very flexible | Structured, requires JSON config |
| Blocking | Cannot block operations | Can block with exit code 2 |
| Best for | Coding style, conventions, preferences | Safety, formatting, verification |
Use both. CLAUDE.md for soft guidance ("prefer named exports"). Hooks for hard requirements ("never touch .env files").
FAQ#
How do I set up my first hook?#
Type /hooks in Claude Code. Choose an event, set a matcher, enter a command. Or edit .claude/settings.json directly.
Can hooks modify tool inputs before execution?#
Yes. PreToolUse hooks can return an updatedInput field in their JSON output to modify tool arguments before execution. Useful for path correction or secret redaction.
Do hooks work in headless mode (claude -p)?#
Yes. Hooks fire in both interactive and headless mode.
What happens if a hook times out?#
The hook is killed and treated as a non-blocking error. The operation proceeds normally, unless the hook sets "onFailure": "block" (v2.1.295 and later), in which case the action is blocked. For a security guard, set a short timeout and onFailure together.
Can I use hooks to auto-approve permissions?#
Yes. A PreToolUse hook returning {"hookSpecificOutput": {"permissionDecision": "allow"}} on stdout will skip the permission prompt. This is a safer alternative to --dangerously-skip-permissions because you control exactly which operations get auto-approved. If you mainly want fewer prompts, auto mode (now Claude Code's default for interactive sessions) uses a classifier instead of hooks; see Claude Code auto mode is now the default for how it interacts with your rules.
How do I debug hooks that aren't working?#
Press Ctrl+O in Claude Code to open the transcript. Hook errors and output appear there. Common issues: wrong case in matcher names, commands not found in PATH, and syntax errors in the JSON config.
Can I have multiple hooks for the same event?#
Yes. Multiple hook entries under the same event run in parallel. Multiple matchers for the same event each fire independently when their pattern matches.
Are there community hook collections?#
Yes. The disler/claude-code-hooks-mastery repo on GitHub has configurations for all events including security validation and observability. The lasso-security/claude-hooks repo focuses on prompt injection defense.
How do hooks compare to Cursor's hook system?#
Claude Code documents over 30 events and five handler types (command, http, mcp_tool, prompt, agent). Competing tools change quickly, so check each vendor's hook docs before comparing; our fail-open versus fail-closed hooks comparison covers Codex, Cursor and Gemini CLI.
Continue Reading#
- Claude Code - plans, install routes, and alternatives on one page
- Claude Code Power User course - wiring deterministic hooks, plus skills, subagents, and headless runs
Get the next deep dive like this in your inbox
One email a week on Claude Code and the rest of the AI dev stack. Free.
Read next on Claude Code
What Is Claude Code? The Complete Guide for 2026
Claude Code is Anthropic's AI coding agent for terminal, IDE, desktop and browser. What it does, how it works, setup, pricing, MCP, skills, hooks and subagents.
15 min read60 Claude Code Tips and Tricks for Power Users
60 Claude Code tips and tricks for power users: sub-agents, hooks, worktrees, MCP, custom agents, keyboard shortcuts and features most developers miss.
25 min readHow to Use Claude Code with Next.js
A practical guide to using Claude Code in Next.js projects. CLAUDE.md config for App Router, common workflows, sub-agents, MCP servers, and TypeScript tips that actually save time.
14 min readTechnical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.








