Hooks
Agents and skills are about what Claude does. Hooks are about what happens around what Claude does — before, during, and after every tool call, and at key lifecycle moments like session start, agent spawn, and compaction. Hooks are the structural enforcement layer: they cannot be overridden by prompts, user instructions, or even 'ignore previous instructions' attacks. If a PreToolUse hook exits with code 2, the tool call is blocked — period.
Claude Code exposes 26 distinct lifecycle events where hooks can fire. These cover the full lifecycle from session startup to shutdown, with events for tool calls, permission decisions, agent management, configuration changes, and context compaction. Four different handler types — shell commands, HTTP webhooks, prompt injection, and agent handlers — let you choose the right mechanism for each enforcement need.
The most important events to understand are the tool call events, which fire on every interaction between Claude and the outside world:
- 🛡️ PreToolUse — Validate, block, or modify
- ⚙️ Tool Executes — Read, Edit, Bash, etc.
- 📋 PostToolUse — Test, format, log, notify
- ⏹️ Stop — Summarize session
Beyond tool calls, hooks fire at session-level events (SessionStart, SessionEnd, InstructionsLoaded), agent events (SubagentStart, SubagentStop, TeammateIdle, TaskCompleted), permission events (PermissionRequest, Elicitation, ElicitationResult), context events (PreCompact, PostCompact), and infrastructure events (ConfigChange, WorktreeCreate, WorktreeRemove). The full 26-event lifecycle gives you structural control over every phase of Claude's operation.
Not every hook needs to be a shell script. Claude Code supports four handler types, each suited for different use cases:
| Icon | Handler | Description |
|---|---|---|
| 💻 | command (shell) | Shell command executed locally. Most common type. Exit code 0 = allow, 2 = block. Stderr becomes block reason. |
| 🌐 | http (webhook) | HTTP POST to a URL with hook payload as JSON. Use for remote logging, CI triggers, audit services. |
| 💬 | prompt | Injects text into conversation context. No code execution — adds instructions Claude reads and follows. |
| 🤖 | agent | Launches a sub-agent for complex reasoning about allow/block decisions. Most powerful, highest latency. |
Hooks are registered in settings.json (team-shared) or settings.local.json (personal). Each hook entry specifies an event, an optional matcher string (regex), and a hooks array of handler objects:
Hook registration with different handler types
{
"hooks": {
// BEFORE tool executes — can block (exit 2) or allow (exit 0)
"PreToolUse": [
{
// Fires before any Edit or Write tool call
// Runs a local script to validate the edit is safe
"matcher": "Edit|Write",
"hooks": [{"type": "command", "command": ".claude/hooks/PreToolUse/validate-edit.sh"}]
},
{
// Fires before any Bash tool call
// Sends the command to a remote audit service via HTTP POST
"matcher": "Bash",
"hooks": [{"type": "http", "url": "https://audit.internal/api/hook"}]
}
],
// AFTER tool executes — for quality gates and logging
"PostToolUse": [
{
// After every Edit — auto-format the changed file
"matcher": "Edit",
"hooks": [{"type": "command", "command": "npm run lint -- --fix"}]
}
],
// When Claude finishes all work — no matcher needed
"Stop": [
{
// Generate a session summary (files changed, cost, etc.)
"hooks": [{"type": "command", "command": ".claude/hooks/Stop/session-summary.sh"}]
}
],
// When session starts — once: true means it runs only once
"SessionStart": [
{
// One-time environment setup (check dependencies, etc.)
"hooks": [{"type": "command", "command": ".claude/hooks/SessionStart/setup-env.sh", "once": true}]
}
]
}
}The matcher field is a regex string matched against the tool name: Edit, Write, Bash, Read, Glob, Grep, Agent, etc. Use | for multiple tools (e.g. "Edit|Write"). Omitting the matcher matches all tools. The hooks array contains handler objects with a type field. The once: true flag ensures the hook fires only once per session — useful for SessionStart initialization.
Note: The directory structure .claude/hooks/PreToolUse/ is an organizational convention, not a requirement. Claude Code does not auto-discover hook scripts by folder name — it only runs what is registered in settings.json. You could store hook scripts anywhere; the convention exists so developers can find them by browsing the directory tree. This is different from other .claude/ paths where naming and location are significant:
| Path | Name/Location Matters? | Why |
|---|---|---|
| Hook scripts | No — any path works | Only the path in settings.json matters |
.claude/settings.json | Yes — exact path | Claude Code looks for this specific file for project settings |
.claude/settings.local.json | Yes — exact path | Personal settings, must be this name (gitignored) |
CLAUDE.md | Yes — exact name | Must be CLAUDE.md at project root or .claude/CLAUDE.md |
.claude/rules/*.md | Yes — directory + extension | Must be .md files in .claude/rules/ to be discovered |
.claude/agents/*.md | Yes — directory + extension | Must be .md files in .claude/agents/ to be discoverable as agents |
.claude/skills/*/SKILL.md | Yes — exact entrypoint | Each skill needs a SKILL.md file inside a named directory |
.mcp.json | Yes — exact name + location | Must be at project root for project-scoped MCP configuration |
For PreToolUse command hooks, the exit code determines what happens to the tool call:
| Exit Code | Effect | Use Case |
|---|---|---|
0 | Allow — tool call proceeds normally | Input passed validation, no issues found |
2 | Block — tool call is prevented, Claude sees stderr | Dangerous operation detected, secrets access attempted |
| Any other | Error — tool call proceeds, error is logged | Hook itself failed, but we don't want to block work |
When a hook exits with code 2, whatever the hook wrote to stderr becomes the block reason that Claude sees. This lets your hooks provide specific, actionable feedback:
PreToolUse hook with structured blocking
#!/bin/bash
# .claude/hooks/PreToolUse/block-secrets.sh
# TOOL_NAME and TOOL_INPUT are provided as environment variables
if echo "$TOOL_INPUT" | grep -qiE '\.env|secret|credential|password'; then
echo "BLOCKED: Attempted access to sensitive file pattern." >&2
echo "Use deny rules in settings.json for permanent protection." >&2
exit 2
fi
exit 0Timing distinction: PostToolUse hooks always run after the tool — their exit code is logged but does not affect the operation. Use PostToolUse for testing, formatting, and logging. Use PreToolUse for prevention. This timing distinction is the most common source of hook security mistakes.
Command hooks receive context about the tool call through environment variables:
| Variable | Description | Available In |
|---|---|---|
TOOL_NAME | Name of the tool being called (Edit, Bash, Read, etc.) | PreToolUse, PostToolUse |
TOOL_INPUT | JSON string of the tool's input parameters | PreToolUse, PostToolUse |
TOOL_OUTPUT | JSON string of the tool's output (result) | PostToolUse only |
SESSION_ID | Unique identifier for the current session | All hooks |
PROJECT_DIR | Absolute path to the project root | All hooks |
CLAUDE_ENV_FILE | Path to env file for SessionStart hooks to set env vars | SessionStart |
HTTP hooks receive the same information as a JSON POST body. Agent hooks receive the context as their initial prompt. Prompt hooks inject text directly — they do not receive environment variables but can reference the hook event context in their prompt template.
The most impactful hooks in production fall into three categories — security enforcement, quality gates, and operational visibility:
| Icon | Hook | Description |
|---|---|---|
| 🛡️ | block-secrets.sh | PreToolUse — Scans tool inputs for .env, secret, credential patterns. Exit 2 blocks access before execution. Your first line of defense. |
| 🔍 | prompt-injection-detector.sh | PreToolUse — Scans for injection patterns ('ignore previous', encoded payloads, social engineering). Blocks malicious content in tool inputs. |
| ✨ | auto-formatter.sh | PostToolUse — Runs prettier/eslint/black after every Edit or Write. Zero manual formatting needed. Code is always clean. |
| 🧪 | test-runner.sh | PostToolUse — Runs npm test / pytest after every code change. Immediate red/green feedback loop. Claude sees test results and self-corrects. |
| 📝 | audit-logger.sh | PostToolUse — Logs all tool calls with timestamp, file, tool name, and user to immutable audit trail. Essential for compliance. |
| 📢 | slack-notifier.sh | Stop — Posts summary of session (files changed, tests run, cost) to team Slack channel. Team visibility without manual reporting. |
Hooks are structural — they cannot be overridden by prompts, user instructions, or even 'ignore previous instructions' attacks. That is the point. Use PreToolUse for prevention (blocks before execution). Use PostToolUse for quality and logging (runs after execution). This timing distinction is the most common source of hook security mistakes.