Claude Code /hooks: Make AI Follow Your Rules
Table of Contents
- What Is /hooks
- Core Concepts
- Events
- Matchers
- Four Hook Types
- How to Use /hooks
- Configuring Hooks in settings.json
- Basic Structure
- Configuration Sources and Priority
- The if Condition Filter
- Practical Examples
- Example 1: Auto-Approve with Logging
- Example 2: Alert on Sensitive File Changes
- Example 3: AI Review for Dangerous Commands
- Example 4: Send Summary on Session End
- Example 5: Run-Once Initialization Hook
- Hook Input and Output
- Input (stdin)
- Output (stdout JSON)
- Async Hooks
- Permission Decision Priority
- Enterprise Controls
- Practical Tips
- Tip 1: Use Command Hooks for Lightweight Checks
- Tip 2: Use if to Narrow the Trigger Scope
- Tip 3: Use updatedInput for Command Enhancement
- Tip 4: Use once to Avoid Repetition
- Tip 5: Don’t Set Timeouts Too Long
- Final Thoughts
What Is /hooks
When using Claude Code, have you ever wished for:
- Automatically running lint before Claude Code executes
git push? - Sending a summary to Slack when a session ends?
- Having another AI review the code before Claude Code deletes a file?
That’s exactly what /hooks does. It’s Claude Code’s event hook system — automatically executing your predefined logic when specific events occur.
If /permissions controls “whether something can be done,” then /hooks controls “what else happens before and after it’s done.”
Core Concepts
Events
The hook system revolves around “events.” Claude Code triggers various events during operation, and you can attach hooks to any of them.
Overview of common events:
| Event | When It Fires | Match Dimension |
|---|---|---|
| PreToolUse | Before tool execution | Tool name (e.g., Bash, Edit) |
| PostToolUse | After tool execution | Tool name |
| PostToolUseFailure | After tool execution fails | Tool name |
| UserPromptSubmit | When user submits a message | — |
| Notification | Notification events | Notification type |
| SessionStart | Session begins | Source (startup/resume/clear/compact) |
| SessionEnd | Session ends | End reason |
| Stop | Claude stops responding | — |
| SubagentStart | Sub-agent launches | Agent type |
| SubagentStop | Sub-agent stops | Agent type |
| PreCompact | Before context compaction | — |
| PostCompact | After context compaction | — |
| PermissionRequest | Permission prompt appears | Tool name |
| PermissionDenied | Permission denied | Tool name |
| ConfigChange | Configuration changes | Config source |
| FileChanged | File changes | Filename |
| CwdChanged | Working directory changes | — |
| InstructionsLoaded | Instruction files loaded | Load reason |
| Elicitation | MCP server asks a question | Server name |
| ElicitationResult | MCP question result | Server name |
| Setup | Initialization | Trigger type (init/maintenance) |
| WorktreeCreate | Worktree created | — |
| WorktreeRemove | Worktree removed | — |
| TaskCreated | Task created | — |
| TaskCompleted | Task completed | — |
| TeammateIdle | Teammate idle | — |
27 events in total, covering virtually every key moment in Claude Code’s operation.
Matchers
Each event can have multiple hooks attached, using a matcher to determine when they fire.
For example, with the PreToolUse event, you can configure different hooks for different tools:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "echo 'about to run bash'" }]
},
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "echo 'about to modify file'" }]
}
]
}
}
Three matching patterns are supported:
| Pattern | Example | Meaning |
|---|---|---|
| Exact match | "Bash" | Matches only the Bash tool |
| Pipe-separated | "Bash|Write|Edit" | Matches any of these |
| Regular expression | "Bash|mcp__.*" | Matches Bash or any MCP tool |
Leave empty or use "*" to match all cases for that event.
Four Hook Types
1. command — Shell Command
The most basic and commonly used type — directly executes a shell command:
{
"type": "command",
"command": "npm run lint",
"timeout": 30000
}
Hooks receive JSON context via stdin (tool name, tool input, session ID, etc.), return results via stdout, and control behavior via exit codes:
| Exit Code | Meaning |
|---|---|
| 0 | Success — stdout content is added to conversation context |
| 2 | Blocking — stderr content informs Claude, operation is blocked |
| Other | Non-blocking error — stderr shown as a warning |
2. prompt — Single-Turn AI Judgment
Uses a lightweight AI model for single-turn evaluation — ideal for “smart review” scenarios:
{
"type": "prompt",
"prompt": "Check if this command might delete important files. If risky, return {\"decision\": \"block\", \"reason\": \"may delete important files\"}, otherwise return {\"decision\": \"approve\"}",
"timeout": 30000
}
Uses a small, fast model by default (typically Haiku). You can specify a different model via the model field.
3. agent — Multi-Turn AI Agent
When single-turn evaluation isn’t enough, use the Agent type — it launches a sub-agent capable of multi-turn conversation and tool use:
{
"type": "agent",
"prompt": "Review this code for security issues. If problems are found, explain them in detail",
"timeout": 60000
}
Agents can run up to 50 conversation turns with a default timeout of 60 seconds.
4. http — HTTP Request
Sends a POST request to an external service — ideal for CI/CD integration, notification systems, audit logs, etc.:
{
"type": "http",
"url": "https://your-webhook.example.com/hook",
"headers": {
"Authorization": "Bearer $API_TOKEN"
},
"allowedEnvVars": ["API_TOKEN"],
"timeout": 600000
}
HTTP hooks include SSRF protection. The request body is JSON-formatted hook input. Headers support environment variable interpolation, but variables must be explicitly declared in allowedEnvVars.
How to Use /hooks
In Claude Code interactive mode, type:
/hooks
This opens a read-only browsing panel that displays all active hooks organized by event. You can:
- Browse hierarchically: Events → Matchers → Individual hooks
- View detailed configuration for each hook (type, command, timeout, etc.)
- See which configuration source each hook comes from (User / Project / Local / Plugin / Session)
Note: The /hooks command itself is read-only. To add or modify hooks, edit settings.json.
Configuring Hooks in settings.json
Basic Structure
{
"hooks": {
"EventName": [
{
"matcher": "match pattern (optional)",
"hooks": [
{
"type": "command | prompt | agent | http",
"command": "...",
"timeout": 30000
}
]
}
]
}
}
Configuration Sources and Priority
Hooks can be configured at different levels, and hooks from all levels are all executed (merged, not overridden):
| Source | File Path |
|---|---|
| User | ~/.claude/settings.json |
| Project | <project>/.claude/settings.json |
| Local | <project>/.claude/settings.local.json |
| Plugin | ~/.claude/plugins/*/hooks/hooks.json |
| Managed | Enterprise managed policy |
This differs from permission rules — permissions use “later overrides earlier,” but hooks merge and all execute.
The if Condition Filter
All hook types support an if field for more precise filtering using permission rule syntax:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo 'dangerous command detected'",
"if": "Bash(rm -rf:*)"
}
]
}
]
}
}
This hook only fires when the Bash tool is about to execute a command starting with rm -rf. The if syntax is the same as the rule format in /permissions.
Practical Examples
Example 1: Auto-Approve with Logging
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo '{\"decision\": \"approve\"}'"
}
]
}
]
}
}
Example 2: Alert on Sensitive File Changes
{
"hooks": {
"FileChanged": [
{
"matcher": ".env|.envrc",
"hooks": [
{
"type": "command",
"command": "echo 'Warning: sensitive config file modified' | tee /dev/stderr && exit 2"
}
]
}
]
}
}
When .env or .envrc files are modified, an automatic warning is triggered. Exit code 2 blocks further operations.
Example 3: AI Review for Dangerous Commands
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "prompt",
"prompt": "Analyze the command from stdin. If it could cause irreversible data loss (e.g., rm -rf, DROP TABLE), return {\"decision\": \"block\", \"reason\": \"...\"}, otherwise return {\"decision\": \"approve\"}"
}
]
}
]
}
}
Example 4: Send Summary on Session End
{
"hooks": {
"SessionEnd": [
{
"hooks": [
{
"type": "http",
"url": "https://hooks.slack.com/services/xxx/yyy/zzz",
"timeout": 10000
}
]
}
]
}
}
Example 5: Run-Once Initialization Hook
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo 'Session initialized at $(date)'",
"once": true
}
]
}
]
}
}
once: true means this hook runs only once per session.
Hook Input and Output
Input (stdin)
Each hook receives a JSON object via stdin containing common fields and event-specific fields:
Common fields:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/your/project",
"permission_mode": "default"
}
Event-specific fields:
| Event | Additional Fields |
|---|---|
| PreToolUse | tool_name, tool_input |
| PostToolUse | tool_name, tool_input, tool_output |
| UserPromptSubmit | user_prompt |
| Notification | notification_type, message |
| SessionStart | source |
| SessionEnd | reason |
| FileChanged | file_path, event |
Output (stdout JSON)
Hooks can output structured JSON to control Claude Code’s behavior:
{
"continue": true,
"decision": "approve",
"reason": "The command appears safe",
"suppressOutput": false,
"systemMessage": "Note: this command has been automatically reviewed"
}
Core fields:
| Field | Type | Description |
|---|---|---|
continue | boolean | Whether to continue execution |
decision | string | "approve" or "block" |
reason | string | Human-readable explanation |
suppressOutput | boolean | Whether to hide hook output |
systemMessage | string | System message injected into the conversation |
PreToolUse-specific output:
{
"hookSpecificOutput": {
"permissionDecision": "allow",
"updatedInput": { "command": "npm test -- --coverage" },
"additionalContext": "Automatically added coverage parameter"
}
}
With updatedInput, you can modify the tool’s input before execution — one of the most powerful capabilities of the hook system.
Async Hooks
Some hooks don’t need to block the main flow. For notifications or logging, you don’t want Claude Code waiting for completion.
Two ways to enable async:
Method 1: Configuration Declaration
{
"type": "command",
"command": "curl -X POST https://example.com/log",
"async": true,
"asyncRewake": true
}
async: true runs the hook in the background. asyncRewake: true wakes the agent when the async hook completes to process the result.
Method 2: Runtime Declaration
The hook outputs {"async": true} as the first line of stdout to switch to async mode. This is useful when you need to decide at runtime whether to go async.
Permission Decision Priority
When multiple PreToolUse hooks make decisions about the same operation, the priority is:
deny > ask > allow
If any single hook says deny, the operation is rejected.
Important: A hook’s allow decision does NOT bypass deny/ask permission rules in settings.json. In other words, you cannot use hooks to “unlock” operations forbidden by the permission system.
Enterprise Controls
Enterprise administrators can control the hook system via managed policies:
| Policy | Effect |
|---|---|
allowManagedHooksOnly: true | Only managed hooks execute; user and project hooks are ignored |
disableAllHooks: true | All hooks disabled (including managed ones) |
When non-managed settings set disableAllHooks, only non-managed hooks are disabled — admin hooks can never be disabled by regular users.
Practical Tips
Tip 1: Use Command Hooks for Lightweight Checks
Shell command hooks start fast with minimal overhead — ideal for format checks, file existence validation, and other lightweight operations. Exit code 2 is your “emergency brake” — use it anytime to block an operation.
Tip 2: Use if to Narrow the Trigger Scope
Don’t trigger hooks for every Bash command. Use the if field for precise filtering:
{
"type": "command",
"command": "run-safety-check.sh",
"if": "Bash(docker:*)"
}
This triggers the safety check only for Docker commands, avoiding unnecessary overhead.
Tip 3: Use updatedInput for Command Enhancement
PreToolUse hooks can modify tool input. For example, automatically adding --save-exact to all npm install commands:
{
"type": "command",
"command": "read input && echo '{\"hookSpecificOutput\": {\"updatedInput\": {\"command\": \"'$(echo $input | jq -r .tool_input.command)' --save-exact\"}}}'"
}
Tip 4: Use once to Avoid Repetition
For initialization logic that only needs to run once (environment checks, dependency installation), add once: true to prevent repeated execution on every trigger.
Tip 5: Don’t Set Timeouts Too Long
Default timeouts: command has no limit, prompt 30 seconds, agent 60 seconds, http 10 minutes. Set a reasonable timeout based on actual needs to prevent hooks from stalling and slowing down the entire session.
Final Thoughts
The hook system is the most flexible extension mechanism in Claude Code.
/permissions answers “can it be done,” while /hooks answers “what else should happen when it’s done.” Master hooks, and you can turn Claude Code into a highly automated development workflow that follows your team’s standards.
But don’t over-engineer — start with one or two simple command hooks, like “run lint before git push” or “send notification on session end.” Once you’re comfortable with the hook input/output mechanism, gradually add more complex prompt and agent hooks.
Start simple, grow as needed.
Related Articles
Claude Code Agent Loop: Dissecting the Heart of an AI Coding Assistant
How does Claude Code understand your requests, invoke tools, and self-recover step by step? A source-code deep dive into the Agent Loop's core architecture — streaming responses, parallel tool execution, auto-compaction, and error recovery.
Claude Code settings.json Explained (1): Where Config Files Live and Who Wins
A complete guide to Claude Code's configuration file system — five config sources, their file paths, priority rules, array merging vs value overriding, and enterprise managed settings delivery.
Claude Code settings.json Deep Dive (Part 2): The Permissions System
A thorough breakdown of Claude Code's permissions configuration — allow/deny/ask rule arrays, wildcard syntax, MCP tool permissions, defaultMode options, and additionalDirectories.
Claude Code settings.json Deep Dive (Part 3): The Hooks System
A thorough breakdown of Claude Code's hooks configuration — four hook types, core events (PreToolUse/PostToolUse/Stop/Notification), stdin/stdout protocol, exit code semantics, and practical examples.
Claude Code settings.json Deep Dive (4): env, Models, Auth, and Other Useful Fields
A comprehensive guide to the remaining settings.json fields in Claude Code — env variable injection, model configuration, authentication helpers, Git attribution, session cleanup, language and UI, thinking depth, auto-updates, memory system, and more.