Lifecycle Hooks
Hooks are deterministic scripts that execute at specific points in an agent’s lifecycle. Unlike guidelines or system prompts — which the model may follow — hooks always run. They provide guaranteed automation: every tool call, every session start, every context compaction event can trigger arbitrary code that the agent cannot skip or override.
Universal Hook Architecture
graph TD
A["User Input"] --> B["PreProcess Hook"]
B --> C["Agent Reasoning"]
C --> D["Tool Selection"]
D --> E["PreToolUse Hook"]
E --> F{"Gate"}
F -->|"exit 0 — allow"| G["Tool Execution"]
F -->|"exit 2 — block"| C
G --> H["PostToolUse Hook"]
H --> I["Agent Response"]
I --> J{"Continue?"}
J -->|"Yes"| C
J -->|"No"| K["Output to User"]
Cross-Platform Event Mapping
| Event | Claude Code | OpenAI Codex | Google ADK | LangGraph |
|---|---|---|---|---|
| Before Tool | PreToolUse | Guardrails (input) | Before-tool callbacks | Middleware nodes |
| After Tool | PostToolUse | Guardrails (output) | After-tool callbacks | Post-processing nodes |
| Session Start | SessionStart | Session init | Session creation | Graph entry node |
| Permission | PermissionRequest | Approval Policy | Action Confirmations | HITL nodes |
| Completion | Stop | Run completion | Agent termination | Terminal nodes |
| Context Overflow | PreCompact/PostCompact | Compaction API | N/A | State summarization |
Hook Types
Hooks come in four flavors, ordered by complexity and latency. On failure, hooks default to fail-open (allow the call); set "on_failure": "fail-closed" for security-critical hooks.
1. Command Hooks (Shell Scripts)
A shell command runs, inspects the input via stdin, and returns an exit code.
{
"hooks": {
"PreToolUse": [
{
"type": "command",
"command": "/usr/local/bin/check-tool-safety.sh",
"timeout_ms": 5000
}
]
}
}
2. HTTP Hooks (Webhooks)
The hook sends the event payload to an HTTP endpoint and acts on a JSON response containing a decision field (allow or block).
{
"hooks": {
"PreToolUse": [
{
"type": "http",
"url": "https://policy.internal.corp/v1/evaluate",
"method": "POST",
"headers": {
"Authorization": "Bearer ${POLICY_TOKEN}"
},
"timeout_ms": 10000
}
]
}
}
3. LLM Hooks (Prompt-Based)
A second model evaluates the action — useful when the decision requires judgment that cannot be reduced to a regex or a lookup table.
{
"hooks": {
"PreToolUse": [
{
"type": "llm",
"model": "claude-sonnet-4-20250514",
"prompt": "You are a security reviewer. Evaluate the following tool call and decide if it is safe. Respond with JSON: {\"decision\": \"allow\"} or {\"decision\": \"block\", \"reason\": \"...\"}.",
"timeout_ms": 30000
}
]
}
}
4. Agent Hooks (Sub-Agent Validation)
A full sub-agent — with its own tool access — validates the action, enabling multi-step reasoning such as inspecting both a schema and its data before approving a migration.
{
"hooks": {
"PreToolUse": [
{
"type": "agent",
"agent": {
"model": "claude-sonnet-4-20250514",
"system": "You are a database migration safety agent. You have access to tools. Verify that the proposed migration will not cause data loss.",
"tools": ["Read", "Bash"],
"max_turns": 5
},
"timeout_ms": 120000
}
]
}
}
Hook Input/Output Protocol
Input
The runtime sends a JSON payload via stdin (for command hooks) or request body (for HTTP hooks):
{
"event": "PreToolUse",
"session_id": "sess_abc123",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/build-artifacts"
},
"timestamp": "2026-03-28T14:32:01Z",
"context": {
"working_directory": "/Users/x/project",
"session_source": "cli",
"turn_number": 7,
"model": "claude-opus-4-6"
}
}
Output
| Exit Code | Meaning |
|---|---|
0 | Proceed. The tool call is allowed. |
2 | Block. The tool call is rejected. The agent sees the reason and can retry with a different approach. |
Any other exit code is treated as a hook failure. Optional JSON on stdout allows structured feedback:
{
"decision": "block",
"reason": "Command matches destructive pattern: rm -rf on non-tmp path.",
"suggestions": [
"Use a more targeted rm command",
"Move files to trash instead of deleting"
]
}
When the hook blocks, the reason field is injected into the agent’s context
so it understands why the action was rejected and can adapt.
Matchers
Matchers are regex-based filters that control when a hook activates. All matcher fields must match (logical AND). For OR logic, define multiple hook entries.
{
"hooks": {
"PreToolUse": [
{
"matcher": {
"tool_name": "^(Bash|Write)$"
},
"type": "command",
"command": "/usr/local/bin/check-destructive.sh"
}
]
}
}
Practical Examples
Block Destructive Commands
Prevent force-pushes to main, destructive production database operations, and similar dangerous commands before they execute.
#!/usr/bin/env bash
# hooks/block-destructive.sh
set -euo pipefail
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
if [[ "$TOOL_NAME" != "Bash" ]]; then
exit 0
fi
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
# Block force-pushes to main
if echo "$COMMAND" | grep -qP 'git\s+push\s+.*--force.*\b(main|master)\b'; then
jq -n '{"decision":"block","reason":"Force-push to main/master is not allowed."}'
exit 2
fi
# Block production database access
if echo "$COMMAND" | grep -qPi '(DROP|TRUNCATE|DELETE\s+FROM)\s.*(prod|production)'; then
jq -n '{"decision":"block","reason":"Destructive operations on production databases are prohibited."}'
exit 2
fi
exit 0
Configuration:
{
"hooks": {
"PreToolUse": [
{
"matcher": { "tool_name": "^Bash$" },
"type": "command",
"command": "bash hooks/block-destructive.sh"
}
]
}
}
Auto-Format After File Writes
Run a formatter every time the agent writes a file, so the codebase stays consistent without relying on the model to format correctly.
#!/usr/bin/env bash
# hooks/auto-format.sh
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ -z "$FILE_PATH" ]]; then
exit 0
fi
# Only format known file types
case "$FILE_PATH" in
*.ts|*.tsx|*.js|*.jsx|*.json|*.css|*.md)
npx prettier --write "$FILE_PATH" 2>/dev/null || true
;;
*.py)
python -m black "$FILE_PATH" 2>/dev/null || true
;;
*.go)
gofmt -w "$FILE_PATH" 2>/dev/null || true
;;
esac
exit 0
Configuration:
{
"hooks": {
"PostToolUse": [
{
"matcher": {
"tool_name": "^Write$"
},
"type": "command",
"command": "bash hooks/auto-format.sh"
}
]
}
}
Secret Detection on Commit
Scan staged files for secrets before the agent can commit them.
#!/usr/bin/env bash
# hooks/secret-scan.sh
set -euo pipefail
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Only trigger on git commit commands
if ! echo "$COMMAND" | grep -qP 'git\s+commit'; then
exit 0
fi
# Run secret detection on staged files
STAGED_FILES=$(git diff --cached --name-only 2>/dev/null)
if [[ -z "$STAGED_FILES" ]]; then
exit 0
fi
SECRETS_FOUND=0
REPORT=""
while IFS= read -r file; do
if [[ ! -f "$file" ]]; then
continue
fi
# Check for common secret patterns
MATCHES=$(grep -nPi \
'(AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9]{48}|ghp_[a-zA-Z0-9]{36}|-----BEGIN (RSA |EC )?PRIVATE KEY-----|password\s*=\s*["\x27][^"\x27]{8,})' \
"$file" 2>/dev/null || true)
if [[ -n "$MATCHES" ]]; then
SECRETS_FOUND=1
REPORT+="$file:\n$MATCHES\n\n"
fi
done <<< "$STAGED_FILES"
if [[ "$SECRETS_FOUND" -eq 1 ]]; then
jq -n \
--arg reason "Potential secrets detected in staged files. Remove them before committing." \
--arg details "$REPORT" \
'{"decision":"block","reason":$reason,"details":$details}'
exit 2
fi
exit 0