Intercept everything. Run custom logic before and after every tool execution.
β Back to Main | β MCP Integration
Hooks are shell commands that run at tool execution boundaries. They give you the power to:
- Block dangerous operations before they execute
- Log every tool call for auditing
- Transform inputs or outputs programmatically
- Enforce custom policies beyond the built-in permission model
flowchart LR
REQ["Tool Request"] --> PRE["β‘ PreToolUse"]
PRE -->|"exit 0: Allow"| EXEC["π Execute Tool"]
PRE -->|"exit 2: Deny"| BLOCK["β Blocked"]
EXEC --> POST["β‘ PostToolUse"]
POST --> RESULT["π¨ Result"]
| Event | When | Can Block? |
|---|---|---|
PreToolUse |
Before tool execution | β Yes (exit code 2) |
PostToolUse |
After tool completion | β No (informational) |
flowchart TD
START["Tool execution requested"] --> CONFIG{"Hook configured<br/>for this event?"}
CONFIG -->|"No"| SKIP["Skip hooks,<br/>proceed to execution"]
CONFIG -->|"Yes"| SPAWN["Spawn subprocess:<br/>shell -lc 'command'"]
SPAWN --> STDIN["Pass JSON payload<br/>via stdin"]
STDIN --> ENV["Set environment<br/>variables"]
ENV --> WAIT["Wait for process<br/>to complete"]
WAIT --> EXIT{"Exit code?"}
EXIT -->|"0"| ALLOW["β
Allowed<br/>Capture stdout"]
EXIT -->|"2"| DENY["β Denied<br/>Capture stdout as reason"]
EXIT -->|"Other"| WARN["β οΈ Warning<br/>Continue anyway"]
ALLOW --> NEXT["Continue to<br/>tool execution"]
DENY --> ERROR["Return hook-denied<br/>error as tool result"]
WARN --> NEXT
{
"hook_event_name": "PreToolUse",
"tool_name": "bash",
"tool_input": "rm -rf /tmp/test",
"tool_input_json": "{\"command\": \"rm -rf /tmp/test\"}"
}For PostToolUse, additional fields:
{
"hook_event_name": "PostToolUse",
"tool_name": "bash",
"tool_input": "cargo test",
"tool_output": "test result: ok. 5 passed",
"tool_result_is_error": false
}| Variable | Description |
|---|---|
HOOK_EVENT |
PreToolUse or PostToolUse |
HOOK_TOOL_NAME |
Name of the tool being called |
HOOK_TOOL_INPUT |
Raw input string |
HOOK_TOOL_IS_ERROR |
"true" or "false" (post-hook only) |
HOOK_TOOL_OUTPUT |
Tool output (post-hook only) |
sequenceDiagram
participant RT as π§ Runtime
participant HK as β‘ Hook Script
participant T as π§ Tool
RT->>RT: Tool requested: bash("rm -rf /")
RT->>HK: PreToolUse hook via stdin:<br/>{"tool_name": "bash", "tool_input": "rm -rf /"}
Note over HK: Script checks for dangerous patterns
HK->>HK: Detects "rm -rf /"
HK-->>RT: exit code 2<br/>stdout: "Blocked: destructive command detected"
Note over RT: Tool execution SKIPPED
RT-->>RT: tool_result: {<br/> error: "Hook denied: Blocked: destructive command detected",<br/> is_error: true<br/>}
graph LR
E0["Exit 0<br/>β
ALLOW"] --> D0["Tool execution proceeds.<br/>stdout appended to context."]
E2["Exit 2<br/>β DENY"] --> D2["Tool execution blocked.<br/>stdout becomes error message."]
EX["Exit 1, 3+<br/>β οΈ WARN"] --> DX["Warning logged.<br/>Tool execution proceeds anyway."]
style E0 fill:#22c55e,color:#fff
style E2 fill:#ef4444,color:#fff
style EX fill:#f59e0b,color:#fff
Hooks are defined in .claude.json or .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"command": "python3 /path/to/safety-check.py"
}
],
"PostToolUse": [
{
"command": "bash /path/to/audit-log.sh"
}
]
}
}#!/bin/bash
# Block dangerous bash commands
read -r payload
tool=$(echo "$payload" | jq -r '.tool_name')
input=$(echo "$payload" | jq -r '.tool_input')
if [[ "$tool" == "bash" ]]; then
if echo "$input" | grep -qE "rm -rf|DROP TABLE|format c:"; then
echo "Blocked: Dangerous command pattern detected"
exit 2
fi
fi
exit 0#!/bin/bash
# Log all tool executions
read -r payload
echo "$(date -u +%FT%TZ) $payload" >> ~/.claude/audit.log
exit 0- Session Management β β Persisting conversations
- Config System β β Where hooks are configured
- Permission Model β β Hooks complement permissions