Hooks
Hooks are user-configured shell commands that intercept lifecycle events — tool calls, agent loop turns, notifications, and session end. They're the primary mechanism for adding custom guardrails, logging, and automation.
Hook Events
| Event | When It Fires | Can Veto? |
|---|---|---|
pre_tool_use | Before each tool execution (after permission check) | Yes — can block the tool call |
post_tool_use | After each tool execution completes | No — side effects only |
pre_query | Before each API call to the model | Yes — can block the turn |
post_query | After the model responds | No — side effects only |
on_notification | When a background task completes | No |
stop | When the session ends | No |
Configuration
Hooks are configured in settings.json at multiple scopes:
{
"hooks": {
"pre_tool_use": [
{
"command": "python3 ~/.claude/hooks/audit-tool.py",
"timeout": 5000
}
],
"post_tool_use": [
{
"command": "bash ~/.claude/hooks/log-tool.sh"
}
],
"stop": [
{
"command": "bash ~/.claude/hooks/on-stop.sh"
}
]
}
}
Configuration Scopes
| Scope | File | Priority |
|---|---|---|
| User | ~/.claude/settings.json | Base |
| Project | .claude.json in project root | Overrides user |
| Local | .claude/local.json | Overrides project |
| Enterprise | Managed policy | Highest priority |
Hooks from all scopes are merged — a project can add hooks without removing user-level hooks.
Execution Model
Hooks run as shell subprocesses. They receive JSON on stdin and write JSON to stdout:
Hook Input & Output
pre_tool_use
The hook receives the tool name and input arguments:
{
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/build",
"description": "Clean build directory"
}
}
The hook responds with a decision:
{ "decision": "allow" }
{ "decision": "deny", "message": "Blocked: rm -rf not allowed" }
{
"decision": "allow",
"updated_input": {
"command": "rm -rf /tmp/build --dry-run",
"description": "Clean build directory (dry run)"
}
}
If the hook process exits with a non-zero exit code, the tool call is denied regardless of stdout.
post_tool_use
{
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run tests"
},
"tool_result": {
"exit_code": 0,
"output": "All 42 tests passed"
}
}
Post-hooks cannot modify or veto — they're for logging, notifications, and analytics. Stdout is ignored.
pre_query / post_query
{
"messages": [...],
"system_prompt_length": 12450,
"turn_number": 3
}
Pre-query hooks can return { "decision": "deny" } to block the API call. Post-query hooks receive the model's response for logging.
Practical Examples
Audit Logger
#!/bin/bash
# Log every tool call to a file
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "$TIMESTAMP $TOOL" >> ~/.claude/audit.log
echo '{"decision": "allow"}'
Block Dangerous Commands
#!/usr/bin/env python3
import sys, json
data = json.load(sys.stdin)
if data["tool_name"] == "Bash":
cmd = data["tool_input"].get("command", "")
blocked = ["rm -rf /", "sudo", "chmod 777", "> /dev/sda"]
for pattern in blocked:
if pattern in cmd:
json.dump({
"decision": "deny",
"message": f"Blocked: '{pattern}' is not allowed"
}, sys.stdout)
sys.exit(0)
json.dump({"decision": "allow"}, sys.stdout)
Slack Notification on Completion
#!/bin/bash
# Send Slack notification when session ends
INPUT=$(cat)
curl -s -X POST "$SLACK_WEBHOOK" \
-H 'Content-Type: application/json' \
-d '{"text": "Claude Code session ended"}'
Where Hooks Fire in the Pipeline
Tool Execution Pipeline
Hooks sit between permission checks and actual execution. See Tool Execution for the full pipeline.
Validate input → Check permissions → ⭐ pre_tool_use hooks
↓
Execute tool
↓
⭐ post_tool_use hooks → Return result
Agent Loop
Query hooks wrap each turn of the agent loop:
User input → System prompt assembly → ⭐ pre_query hook
↓
API call (streaming)
↓
⭐ post_query hook → Process response
If a pre_query hook vetoes, the agent loop terminates with hook_stopped.
Hooks vs Plugin Hooks
| Aspect | Settings Hooks | Plugin Hooks |
|---|---|---|
| Config | settings.json | Plugin manifest |
| Language | Any (shell command) | JavaScript/TypeScript |
| Scope | User/project/enterprise | Per-plugin |
| Execution | Subprocess (stdin/stdout) | In-process function call |
| Use case | Personal guardrails, logging | Reusable extension logic |
Plugins can register hooks via their manifest (see Plugins & Skills). These run in-process and have access to the full plugin API, while settings hooks are isolated subprocesses.
Key Source Files
| File | Purpose |
|---|---|
src/utils/hooks/ | Hook execution engine (5+ files) |
src/hooks/ | React hooks (different concept — UI state hooks) |
src/services/plugins/ | Plugin hook registration |