Claude Code hooks are the feature that makes Claude Code programmable in a way no other AI coding tool is. They are shell commands or LLM prompts that fire automatically at specific points in Claude Code's lifecycle — and they are the difference between babysitting your AI assistant and letting it run autonomously with guardrails.
Boris Cherny, engineer and author of Programming TypeScript, captured this in early 2026: "Reflecting on what engineers love about Claude Code, one thing that jumps out is its customizability: hooks, plugins, LSPs, MCPs. Each is an extension point that lets you mold Claude Code to your exact workflow." No other AI coding tool offers this level of programmability.
Why Hooks Are the Killer Feature
The distinction matters. VS Code extensions customize your editor. GitHub Actions customize your CI. Claude Code hooks customize your AI agent. They turn a general-purpose coding assistant into a domain-specific development partner that enforces your team's rules, integrates with your infrastructure, and never forgets a step.
Unlike CLAUDE.md instructions which are advisory and may be ignored during long sessions, hooks are deterministic. They execute every time, guaranteed. This is the critical distinction:
| Mechanism | Behavior | Best For |
|---|---|---|
| CLAUDE.md | Advisory — Claude reads it but may forget in long sessions | Guidelines: 'Prefer named exports', 'Use Bun, not npm' |
| Hooks | Deterministic — executes every time, zero exceptions | Requirements: 'Run eslint after every edit', 'Block writes to migrations/' |
The trust problem hooks solve
You cannot fully trust an LLM to always do the right thing. You should not have to babysit it on every action. Hooks are the middle ground — deterministic guardrails that let you run Claude Code in autonomous mode with confidence. They are the reason power users report running Claude Code with --allowedTools permissions they would never grant without hooks protecting critical paths.
The Three Hook Types
Every hook is one of three types, each suited to different levels of decision complexity.
Command Hooks
Run shell scripts. Receive JSON via stdin, return results through exit codes and stdout. The workhorse — 90% of hooks in the wild are commands.
Prompt Hooks
Send a prompt to a Claude model for single-turn yes/no evaluation. No shell scripting required — the LLM decides. Fast, cheap, no tool access.
Agent Hooks
Spawn a subagent that can read files, search code, and run commands to verify conditions. Up to 50 tool-use turns. The most powerful type.
Choose command hooks for anything deterministic (formatting, blocking, logging). Choose prompt hooks when the decision requires judgment but no file access. Choose agent hooks when verification requires inspecting the actual codebase state.
Hook Events: The Complete Lifecycle
Hooks fire at 14 different lifecycle events. Each corresponds to a specific moment in Claude's execution where you can inject custom behavior. The four most commonly used events are highlighted.
| Event | When It Fires | Can Block? |
|---|---|---|
| SessionStart | Session begins, resumes, or context is compacted | No |
| UserPromptSubmit | You submit a prompt, before Claude processes it | Yes |
| PreToolUse | Before a tool call executes | Yes |
| PermissionRequest | When a permission dialog appears | Yes |
| PostToolUse | After a tool call succeeds | No |
| PostToolUseFailure | After a tool call fails | No |
| Notification | Claude sends a notification | No |
| SubagentStart | A subagent is spawned | No |
| SubagentStop | A subagent finishes | Yes |
| Stop | Claude finishes responding | Yes |
| TeammateIdle | Agent team teammate about to go idle | Yes |
| TaskCompleted | Task marked as completed | Yes |
| PreCompact | Before context compaction | No |
| SessionEnd | Session terminates | No |
Matchers: Filtering When Hooks Fire
Without a matcher, a hook fires on every occurrence of its event. Matchers are regex strings that narrow this down. Use "*", "", or omit the matcher to match everything.
| Event | Matches On | Example Matchers |
|---|---|---|
| PreToolUse / PostToolUse | Tool name | Bash, Edit|Write, mcp__.* |
| SessionStart | How session started | startup, resume, clear, compact |
| SessionEnd | Why session ended | clear, logout, other |
| Notification | Notification type | permission_prompt, idle_prompt |
| SubagentStart / Stop | Agent type | Bash, Explore, Plan, custom names |
| PreCompact | Compaction trigger | manual, auto |
How Hooks Work: Input, Output, Exit Codes
Every hook receives JSON context via stdin when it fires. Your script reads this data, takes action, and tells Claude Code what to do next.
JSON Input
Example: PreToolUse input for a Bash command
{
"session_id": "abc123",
"cwd": "/Users/dev/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}Exit Codes
Exit 0 — Success
Action proceeds. For UserPromptSubmit and SessionStart, stdout is added as context for Claude.
Exit 2 — Block
Action is blocked. Stderr text is fed back to Claude as an error so it can adjust its approach.
Other Codes — Non-blocking
Action proceeds. Stderr is logged in verbose mode (Ctrl+O) but not shown to Claude.
Structured JSON Output
For more control than exit codes alone, exit 0 and print JSON to stdout. PreToolUse hooks can return permissionDecision (allow/deny/ask). PostToolUse and Stop hooks can return decision: "block" with a reason.
PreToolUse JSON output — deny a tool call with feedback
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Use rg instead of grep for performance"
}
}The Security Angle: Programmable Guardrails
When VS Code 1.109 introduced terminal sandboxing in late 2025 to prevent AI agents from running dangerous commands, it validated a problem Claude Code users had already solved with hooks. But hooks go far beyond sandboxing.
| Capability | VS Code Sandboxing | Claude Code Hooks |
|---|---|---|
| Block dangerous commands | Yes (fixed blocklist) | Yes (custom logic) |
| Custom rules per project | No | Yes — different rules per repo |
| Custom rules per directory | No | Yes — protect specific paths |
| Audit logging | No | Yes — log all actions to file |
| Require confirmation | Binary allow/deny | Allow / deny / ask (prompt user) |
| Inspect tool arguments | No | Yes — read full JSON input |
| Integrate with external systems | No | Yes — call any API or service |
Hooks are programmable security policies, not just a blocklist. You write the logic. You decide what gets blocked, what gets logged, and what requires human confirmation. This is the level of control that makes teams comfortable running Claude Code in autonomous mode on production codebases.
Security-focused PreToolUse hook — inspect and block dangerous patterns
#!/bin/bash
# .claude/hooks/security-guard.sh
# Programmable security policy — block destructive commands,
# require review for sensitive operations
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
# Block destructive shell commands
if [ "$TOOL" = "Bash" ]; then
if echo "$COMMAND" | grep -qE 'rm -rf|git push --force|drop table|truncate'; then
echo "BLOCKED: destructive command not allowed" >&2
exit 2
fi
# Log all shell commands to audit trail
echo "$(date -u +%FT%TZ) BASH: $COMMAND" >> .claude/audit.log
fi
# Protect sensitive files from edits
if [ "$TOOL" = "Edit" ] || [ "$TOOL" = "Write" ]; then
if echo "$FILE_PATH" | grep -qE '\.env|migrations/|package-lock\.json|\.git/'; then
echo "BLOCKED: $FILE_PATH is a protected file" >&2
exit 2
fi
fi
exit 0Register as a universal PreToolUse hook
// .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/security-guard.sh"
}
]
}
]
}
}Auto-Format, Block, Notify: Core Patterns
These are the three hooks every Claude Code user should set up on day one. They eliminate the most common friction points.
Typecheck on Every Commit (Recommended)
The highest-value hook you can add today. bun run typecheck:fast uses tsgo (the native TypeScript type checker) to catch type errors in under a second. Run it as a Stop hook so Claude cannot finish a task with broken types.
Stop hook — typecheck with tsgo before Claude finishes
// .claude/settings.json
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "#!/bin/bash\nINPUT=$(cat)\nif [ \"$(echo \"$INPUT\" | jq -r '.stop_hook_active')\" = \"true\" ]; then exit 0; fi\nbun run typecheck:fast 2>&1 || { echo 'Type errors found. Fix before finishing.' >&2; exit 2; }"
}
]
}
]
}
}Why tsgo changes everything
Traditional tsc takes 10-30 seconds on a medium project. tsgo (the native Go port) completes in under a second. This makes typecheck-on-every-commit practical — the hook runs so fast that it feels instant. Add "typecheck:fast": "tsgo --noEmit" to your package.json scripts, then use this hook. Every commit Claude makes will be type-safe.
Pair this with a CLAUDE.md instruction to push to branches:
CLAUDE.md — enforce branch-based workflow
# Workflow
- IMPORTANT: Always push to a branch, never directly to main
- Run `bun run typecheck:fast` after every series of code changes
- Create a PR for review — do not merge directlyAuto-Format After Every Edit
The most popular hook in the community. AI-generated code instantly matches your project's style guide without relying on Claude to remember formatting rules.
PostToolUse hook — auto-format with Prettier
// .claude/settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null"
}
]
}
]
}
}Works with any formatter
Replace npx prettier --write with any command: black for Python, gofmt -w for Go, rustfmt for Rust, mix format for Elixir. The hook extracts the file path from JSON input with jq and passes it to your formatter.
Desktop Notifications
Stop watching the terminal. Get alerted when Claude needs input so you can focus on other work.
Notification hook — macOS native alerts
// ~/.claude/settings.json (applies to all projects)
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}macOS
osascript -e 'display notification "..." with title "Claude Code"'
Linux
notify-send 'Claude Code' 'Needs attention'
Windows
powershell.exe -Command "[Windows.Forms.MessageBox]::Show('Needs attention')"
Stop Hooks as Quality Gates
Stop hooks fire when Claude finishes responding. They can prevent Claude from stopping if conditions are not met. This turns "please remember to run tests" from an advisory suggestion into an enforced requirement.
Stop hook — require passing tests before Claude can finish
#!/bin/bash
# .claude/hooks/require-tests.sh
INPUT=$(cat)
# CRITICAL: Check stop_hook_active to prevent infinite loops
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # Already continued once — let Claude stop
fi
# Run tests
if ! npm test 2>&1; then
echo "Tests failing. Fix before finishing." >&2
exit 2 # Block Claude from stopping
fi
exit 0The infinite loop trap
Always check stop_hook_active in Stop hooks. When it is true, Claude is already continuing because of a previous Stop hook. Exit 0 immediately. Without this check, your hook blocks Claude forever. This is the #1 mistake new hook authors make.
Stop hooks are where hooks become genuinely transformational. They implement what the community calls "quality gates" — automated checks that run every time Claude says it is done. Combined with best practices for task scoping, they close the loop on the "80% problem" where AI solutions are almost right but not quite.
Prompt and Agent-Based Hooks
Not every decision can be made with a shell script. For decisions that require judgment, use prompt or agent hooks.
Prompt Hooks
Single-turn LLM evaluation. The model returns yes/no. Fast, cheap, no tool access. Best for: "Is this commit message descriptive enough?"
Prompt-based Stop hook
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Check if all requested
tasks are complete. If not,
respond with {\"ok\": false,
\"reason\": \"what remains\"}."
}
]
}
]
}
}Agent Hooks
Multi-turn verification with tool access. Spawns a subagent that can read files and run commands. Best for: "Do all tests pass?"
Agent-based Stop hook
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "Verify all unit tests
pass. Run the test suite
and check results.
$ARGUMENTS",
"timeout": 120
}
]
}
]
}
}Both types return the same format: {"ok": true} to allow the action, or {"ok": false, "reason": "..."} to block it. Use prompt hooks when the input data alone is enough to decide. Use agent hooks when you need to verify against the actual state of the codebase.
Hook + MCP Integration
This is where hooks become truly powerful. MCP (Model Context Protocol) servers give Claude access to external tools — databases, APIs, search engines. Hooks let you add guardrails, logging, and automation around those MCP tools.
MCP tools follow the naming pattern mcp__server__tool. Use regex matchers to target specific servers or operations.
Log all GitHub MCP operations
// .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__github__.*",
"hooks": [
{
"type": "command",
"command": "jq -r '"GITHUB: " + .tool_name + " " + (.tool_input | tostring)' >> .claude/mcp-audit.log"
}
]
}
]
}
}Validate search queries before WarpGrep executes
// .claude/settings.json — Hook + MCP integration example
{
"hooks": {
"PostToolUse": [
{
"matcher": "mcp__warpgrep__.*",
"hooks": [
{
"type": "command",
"command": "echo 'Search complete. Results injected into context.' >> .claude/search.log",
"async": true
}
]
}
],
"PreToolUse": [
{
"matcher": "mcp__.*__write.*|mcp__.*__delete.*|mcp__.*__create.*",
"hooks": [
{
"type": "command",
"command": "echo 'MCP write operation detected — logging for audit' >> .claude/mcp-audit.log"
}
]
}
]
}
}Hooks + MCPs = custom development environments
The combination of hooks (deterministic guardrails) and MCPs (tool access) is what makes Claude Code uniquely extensible. Hooks ensure safety and consistency. MCPs provide capability. Together, they let teams build development environments tailored to their exact stack and workflow — something no other AI coding tool supports at this level.
Domain-Specific Automation
Hooks stop being "a feature" and become transformational when you realize they can encode your domain's rules. Claude Code is not just a coding assistant — with hooks, it becomes your coding assistant, shaped to your industry's exact requirements.
Healthcare / HIPAA
PreToolUse hook scans every file write for potential PHI exposure. Blocks edits to files containing patient data patterns. Logs all file access for compliance audit trail.
Fintech / PCI-DSS
PreToolUse hook requires human confirmation for any edit in payment-related directories. PostToolUse hook runs security linter after every code change. Agent Stop hook verifies no secrets in committed code.
Game Development
PostToolUse hook auto-runs asset pipeline after shader or model file edits. PreToolUse hook prevents changes to locked asset files being edited by other team members.
Monorepo / Platform Teams
PreToolUse hooks enforce package boundaries — block edits crossing module ownership lines. PostToolUse hooks auto-run affected package tests. SessionStart hooks inject team-specific context.
Example: HIPAA compliance hook
#!/bin/bash
# .claude/hooks/hipaa-guard.sh
# Block writes to files that may contain PHI patterns
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [ "$TOOL" = "Edit" ] || [ "$TOOL" = "Write" ]; then
# Check if file is in protected patient data directories
if echo "$FILE_PATH" | grep -qE 'patient_data/|phi/|medical_records/'; then
echo "BLOCKED: PHI directory requires manual review" >&2
exit 2
fi
# Check file content for SSN/MRN patterns before allowing write
if [ -f "$FILE_PATH" ]; then
if grep -qE '[0-9]{3}-[0-9]{2}-[0-9]{4}|MRN-[0-9]+' "$FILE_PATH"; then
echo "BLOCKED: File contains potential PHI patterns" >&2
exit 2
fi
fi
fi
# Log all operations for compliance
echo "$(date -u +%FT%TZ) $TOOL: $FILE_PATH" >> .claude/hipaa-audit.log
exit 0Advanced Patterns
Async Hooks for Background Tasks
Set "async": true to run a hook in the background. Claude continues working while the hook executes. Results are delivered on the next conversation turn.
Async PostToolUse — run tests in background
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-tests-async.sh",
"async": true,
"timeout": 300
}
]
}
]
}
}Context Re-Injection After Compaction
When Claude's context is compacted, important details can be lost. A SessionStart hook with a compact matcher re-injects critical context automatically.
SessionStart hook — re-inject context after compaction
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'REMINDER: Use Bun, not npm. Run bun test before commits. Current sprint: auth refactor. See CLAUDE.md for full context.'"
}
]
}
]
}
}Persist Environment Variables
SessionStart hooks have access to CLAUDE_ENV_FILE. Write export statements to this file and they are available in all subsequent Bash commands.
Set environment variables for the session
#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE"
echo 'export DEBUG_LOG=true' >> "$CLAUDE_ENV_FILE"
echo "export PROJECT_ROOT=$(pwd)" >> "$CLAUDE_ENV_FILE"
fi
exit 0Hooks in Skills and Agent Frontmatter
Hooks defined in skill or agent YAML frontmatter are scoped to that component's lifecycle. They only run while the skill/agent is active and are cleaned up when it finishes. This is ideal for security checks or validation that only applies during specific workflows.
Configuration Reference
Hooks live in JSON settings files with three levels of nesting: event, matcher group, and handler.
Full hook structure
{
"hooks": {
"PostToolUse": [ // 1. Hook event
{
"matcher": "Edit|Write", // 2. Matcher (regex)
"hooks": [ // 3. Handler(s)
{
"type": "command",
"command": "npx prettier --write $(jq -r '.tool_input.file_path')"
}
]
}
]
}
}Where to Store Hooks
| Location | Scope | Shareable? |
|---|---|---|
| ~/.claude/settings.json | All your projects | No (local to machine) |
| .claude/settings.json | Single project | Yes (commit to git) |
| .claude/settings.local.json | Single project | No (gitignored) |
| Plugin hooks/hooks.json | When plugin enabled | Yes (bundled with plugin) |
| Skill/agent frontmatter | While component active | Yes (in file) |
| Managed policy | Organization-wide | Yes (admin-controlled) |
The fastest way to create a hook is the /hooks interactive menu. Type /hooks in Claude Code to view, add, and delete hooks without editing JSON directly. You can also ask Claude: "Write a hook that runs eslint after every file edit."
Debugging hooks
If your hook is not firing, check three things: (1) the matcher is case-sensitive — bash does not match Bash, (2) your shell profile (~/.zshrc) might have unconditional echo statements that prepend text to JSON output — wrap them in if [[ $- == *i* ]], and (3) check the hook timeout — the default is 60 seconds and long-running hooks will be killed.
Frequently Asked Questions
What are Claude Code hooks?
Claude Code hooks are user-defined shell commands or LLM prompts that execute automatically at specific points in Claude Code's lifecycle. They provide deterministic control — unlike CLAUDE.md which is advisory, hooks guarantee the action happens every time. They are the extensibility feature that makes Claude Code programmable, letting you enforce security policies, automate formatting, and build domain-specific workflows.
How do I set up my first Claude Code hook?
Type /hooks in Claude Code to open the interactive menu. Select an event, set a matcher, and enter a shell command. You can also add hooks manually to .claude/settings.json or ask Claude: "Write a hook that runs eslint after every file edit."
What is the difference between PreToolUse and PostToolUse hooks?
PreToolUse runs before Claude executes a tool and can block it (exit code 2). Ideal for security guards. PostToolUse runs after a tool completes and cannot undo actions. Ideal for formatting, testing, and logging.
How do Claude Code hooks compare to VS Code terminal sandboxing?
VS Code 1.109 introduced terminal sandboxing with a fixed blocklist. Claude Code hooks provide the same safety but are programmable — custom logic per project, per directory, with audit logging, external API integration, and three-way allow/deny/ask decisions. Hooks are proactive policies where sandboxing is reactive blocking.
How do I prevent a Stop hook from running forever?
Check the stop_hook_active field in the JSON input. When it is true, exit 0 immediately to let Claude stop. Without this check, the hook creates an infinite loop. This is the most common mistake new hook authors make.
Can I use hooks with MCP tools?
Yes. MCP tools follow the naming pattern mcp__server__tool. Use regex matchers like mcp__github__.* to match all tools from a specific server, or mcp__.*__write.* to match write operations across all servers.
What is the recommended typecheck hook for TypeScript projects?
Use bun run typecheck:fast with tsgo (the native Go port of the TypeScript type checker) as a Stop hook. It completes in under a second — fast enough to run on every commit. Traditional tsc takes 10-30 seconds and is too slow for a commit hook. Add "typecheck:fast": "tsgo --noEmit" to your package.json scripts. Pair this with a CLAUDE.md instruction to always push to a branch, never directly to main.
Can Claude Code hooks run asynchronously?
Yes. Set "async": true on a command hook. Claude continues working while the hook runs in the background. Results are delivered on the next conversation turn. Async hooks cannot block actions since the triggering action has already completed.
How do hooks enable domain-specific automation?
Hooks encode your domain's rules as deterministic guardrails. Healthcare teams enforce HIPAA compliance on every file write. Fintech teams require security review for payment code. Game studios auto-run asset pipelines after shader edits. Combined with MCP servers for domain-specific tools, hooks transform Claude Code from a general-purpose assistant into a domain-specific development partner.
Pair Hooks with Intelligent Search
WarpGrep gives Claude Code a search subagent that keeps your context clean — the perfect complement to hooks for building a fully automated development workflow.