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
Hooks give you deterministic control over Claude Code. Auto-format on save, block dangerous commands, run tests before commits, fire desktop notifications. Here's how to set them up.
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.
Official Sources
| Source | What to verify |
|---|---|
| Claude Code hooks docs | Hook types, lifecycle events, 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, 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.
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. Claude Code supports three types, which is more than any competing tool.
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. This is unique to Claude Code. No other AI coding tool lets you delegate hook decisions to an LLM without writing custom code.
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 full list.
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 |
Full 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.
Newsletter
Get the weekly deep dive
Tutorials on Claude Code, AI agents, and dev tools, delivered free every week.
From the archive
Claude Code vs Cursor vs Codex: Which Should You Use?
Apr 2, 2026 • 8 min read
Claude Computer Use: AI That Controls Your Desktop
Apr 2, 2026 • 6 min read
Claude Haiku 4.5: Near-Frontier Intelligence at a Fraction of the Cost
Apr 2, 2026 • 5 min read
Local OpenTelemetry Traces Are Agent Receipts
Apr 2, 2026 • 9 min read
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.
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
Hooks have a default timeout of 60 seconds. For commands that might take longer, set the timeout field explicitly (in milliseconds):
{
"type": "command",
"command": "npm test",
"timeout": 120000
}
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.
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.
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?
Cursor added hooks in v1.7 with 6 events and command-only handlers. Claude Code has 15 events and three handler types (command, prompt, agent). The prompt and agent hook types are unique to Claude Code.
Read next
What Is Claude Code? The Complete Guide for 2026
Claude Code is Anthropic's AI coding agent for terminal, IDE, desktop, and browser workflows. Learn what it does, how it works, pricing, setup, MCP, skills, hooks, and subagents.
15 min read60 Claude Code Tips and Tricks for Power Users
The definitive collection of Claude Code tips - sub-agents, hooks, worktrees, MCP, custom agents, keyboard shortcuts, and dozens of hidden features most developers never discover.
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.









