Skip to main content

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

Tool Pipeline
pre_tool_use
post_tool_use
Agent Loop
pre_query
post_query
Session
on_notification
stop
EventWhen It FiresCan Veto?
pre_tool_useBefore each tool execution (after permission check)Yes — can block the tool call
post_tool_useAfter each tool execution completesNo — side effects only
pre_queryBefore each API call to the modelYes — can block the turn
post_queryAfter the model respondsNo — side effects only
on_notificationWhen a background task completesNo
stopWhen the session endsNo

Configuration

Hooks are configured in settings.json at multiple scopes:

~/.claude/settings.json (user scope)
{
"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

ScopeFilePriority
User~/.claude/settings.jsonBase
Project.claude.json in project rootOverrides user
Local.claude/local.jsonOverrides project
EnterpriseManaged policyHighest 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:

Event fires (e.g. pre_tool_use)
🐚
Spawn hook as shell command
📥
Pipe event data as JSON to stdin
Wait for hook to complete (with timeout)
Exit 0 + allow
Proceed normally
Exit 0 + deny
Block execution
Non-zero exit
Treated as veto

Hook Input & Output

pre_tool_use

The hook receives the tool name and input arguments:

stdin
{
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/build",
"description": "Clean build directory"
}
}

The hook responds with a decision:

stdout — allow
{ "decision": "allow" }
stdout — deny with message
{ "decision": "deny", "message": "Blocked: rm -rf not allowed" }
stdout — allow with modified input
{
"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

stdin
{
"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

stdin (pre_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

~/.claude/hooks/audit-tool.sh
#!/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

~/.claude/hooks/block-dangerous.py
#!/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

~/.claude/hooks/notify-stop.sh
#!/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

AspectSettings HooksPlugin Hooks
Configsettings.jsonPlugin manifest
LanguageAny (shell command)JavaScript/TypeScript
ScopeUser/project/enterprisePer-plugin
ExecutionSubprocess (stdin/stdout)In-process function call
Use casePersonal guardrails, loggingReusable 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

FilePurpose
src/utils/hooks/Hook execution engine (5+ files)
src/hooks/React hooks (different concept — UI state hooks)
src/services/plugins/Plugin hook registration