The Hooks System
From probabilistic to deterministic
Skills instruct Claude what to do. CLAUDE.md provides guidance the agent should follow. Both rely on Claude choosing to comply.
Hooks take a different approach entirely. A hook is a shell command that executes at a specific lifecycle point, every time, without fail. Claude does not decide whether to run it. The hook fires as deterministic application code, not as a suggestion the agent interprets.
This distinction matters for automation that cannot tolerate "usually works."
A PreToolUse hook that blocks writes to .env files does not rely on Claude remembering a policy.
It intercepts the operation at the application level and blocks it with certainty.
A PostToolUse hook that runs Prettier after every edit does not depend on Claude feeling like formatting.
It formats the code every time.
Module 2 introduced hooks as a configuration option. This section covers the full hook system: all events, input/output contracts, and the mechanics that make hooks reliable enough for enterprise automation.
The complete event catalog
Claude Code fires hooks at thirteen lifecycle points. Each event carries specific semantics about when it fires and what control it offers.
| Event | When it fires | Can block |
|---|---|---|
| SessionStart | Session begins or resumes | No |
| UserPromptSubmit | User submits a prompt | Yes |
| PreToolUse | Before tool execution | Yes |
| PermissionRequest | When permission dialog appears | Yes |
| PostToolUse | After tool succeeds | No |
| PostToolUseFailure | After tool fails | No |
| SubagentStart | When spawning a subagent | No |
| SubagentStop | When subagent finishes | Yes |
| Stop | Claude finishes responding | Yes |
| PreCompact | Before context compaction | No |
| SessionEnd | Session terminates | No |
| Notification | Claude Code sends notifications | No |
| Setup | Repository initialization via flags | No |
The "Can block" column indicates whether exit code 2 prevents the operation from proceeding. Events that cannot block still receive the hook's output for logging or notification purposes.
SessionStart
Fires when a session begins (startup), resumes from a previous state (resume), gets cleared (clear), or undergoes compaction (compact).
The matcher field distinguishes these scenarios:
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/load-secrets.sh"
}
]
}
]
}
}SessionStart hooks can inject context by writing to stdout. Any output from the hook is added to the conversation context, making hooks useful for loading dynamic information at session start.
UserPromptSubmit
Fires after the user types a prompt but before Claude processes it. This is the interception point for input validation, logging, or transformation.
With exit code 2, the hook blocks prompt processing entirely, erases the prompt from the interface, and shows stderr to the user. This enables guardrails that prevent certain requests from reaching the agent at all.
PreToolUse and PostToolUse
These two events handle most hook automation scenarios.
PreToolUse fires before any tool executes. The hook receives the tool name and complete input parameters via stdin. Exit code 2 blocks the tool call and feeds stderr back to Claude as an error message, allowing the agent to adjust its approach.
PostToolUse fires after successful tool execution. The hook receives the tool input and output. It cannot block because the operation already completed, but it can perform follow-up actions like formatting, testing, or logging.
PostToolUseFailure fires when a tool fails. This enables different handling for errors than successes—alerting on failures while staying quiet on success, for example.
PermissionRequest
Fires when Claude Code would normally show a permission dialog.
Hooks can auto-approve operations by returning a JSON response with hookSpecificOutput.permissionDecision: "allow", deny them outright with "deny", or pass through to the user with "ask".
This enables policy-based permission handling that evaluates each request programmatically rather than requiring user interaction for every operation.
Stop and SubagentStop
Stop fires when Claude finishes generating a response. SubagentStop fires when a spawned subagent completes.
Both support blocking via exit code 2, which signals the agent to continue working rather than stopping. This enables hooks that validate completion criteria: did the agent actually finish the task, or did it just stop talking?
PreCompact
Fires before Claude Code compresses conversation context.
The matcher distinguishes manual compaction (manual) from automatic compaction triggered by context limits (auto).
Hooks can perform cleanup, save state, or log what context is about to be lost. They cannot prevent compaction from occurring.
Setup
Fires during repository initialization triggered by --init, --init-only, or --maintenance flags.
The matcher distinguishes initialization (init) from maintenance runs (maintenance).
This hook enables automatic project setup: installing dependencies, configuring tools, establishing initial state.
Hook input contract
Every hook receives JSON via stdin containing the event context:
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../session.jsonl",
"cwd": "/Users/project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run tests"
},
"tool_use_id": "toolu_01ABC123..."
}The exact fields vary by event.
PreToolUse includes tool_name and tool_input.
PostToolUse adds tool_output with the execution result.
SessionStart includes the session type (startup, resume, etc.).
Hooks parse this JSON to make decisions.
A common pattern uses jq to extract relevant fields:
#!/bin/bash
input=$(cat /dev/stdin)
tool_name=$(echo "$input" | jq -r '.tool_name')
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
# Block writes to sensitive files
if [[ "$file_path" == *".env"* ]]; then
echo "Cannot modify .env files" >&2
exit 2
fi
exit 0Exit codes and their meaning
| Exit code | Meaning | Behavior |
|---|---|---|
| 0 | Success/Allow | Operation proceeds; stdout parsed for structured output |
| 2 | Block | Operation prevented; stderr fed back to Claude |
| Other | Error | Warning logged; operation proceeds |
Exit code 2 only blocks operations for events that support blocking. For other events, exit code 2 behaves like any non-zero exit: it logs a warning and continues.
The asymmetry is intentional. PreToolUse blocking prevents potentially harmful operations. PostToolUse cannot block because the operation already happened. Stop blocking keeps Claude working when automated criteria say the task is incomplete.
Structured JSON output
Hooks can return JSON to stdout for fine-grained control:
{
"continue": true,
"stopReason": "",
"suppressOutput": false,
"systemMessage": "Warning: approaching sensitive data",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Whitelisted command pattern",
"updatedInput": {
"command": "npm test --ci"
},
"additionalContext": "Note: running in CI mode for deterministic output"
}
}Key fields:
- continue: Whether processing should continue (false stops the session)
- systemMessage: Injected into conversation as a system message
- hookSpecificOutput.permissionDecision: For PermissionRequest, auto-resolves the dialog
- hookSpecificOutput.updatedInput: For PreToolUse, modifies tool parameters before execution
- hookSpecificOutput.additionalContext: Injects context visible to Claude
The updatedInput field enables input transformation.
A PreToolUse hook can intercept rm -rf / and change it to echo "Blocked dangerous command" without fully blocking the operation or causing the agent to interpret the action as a failure.
When a hook returns exit code 2, JSON in stdout is ignored. Only stderr reaches Claude as the error message. Structure your hooks to output JSON only on success (exit 0).
Configuration structure
Hooks live in settings files following the standard hierarchy:
- Enterprise managed settings (highest priority)
- Project settings (
.claude/settings.json) - User settings (
~/.claude/settings.json) - Local project settings (
.claude/settings.local.json)
The configuration structure:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/validate-bash.sh",
"timeout": 30
}
]
}
]
}
}Matchers
The matcher field filters which operations trigger the hook:
- Exact match:
"Write"matches only the Write tool - Alternatives:
"Edit|Write"matches either - Regex:
"Notebook.*"matches NotebookEdit, NotebookRead, etc. - Wildcard:
"*"or""matches all tools - Arguments:
"Bash(npm test*)"matches Bash calls starting withnpm test
Matchers are case-sensitive.
"bash" does not match the Bash tool.
Timeout
Hooks timeout after 60 seconds by default.
The timeout field overrides this, accepting values up to 600 seconds (10 minutes).
Long-running hooks should use appropriate timeouts. A hook that runs a test suite needs more time than one that checks a file extension.
The once flag
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "load-context.sh",
"once": true
}
]
}Setting once: true ensures the hook runs only once per session.
Subsequent events matching the same hook are skipped.
This prevents redundant initialization or loading.
Environment variables
Hooks receive environment variables for context:
| Variable | Description |
|---|---|
CLAUDE_PROJECT_DIR | Absolute path to project root |
CLAUDE_ENV_FILE | Path for persisting environment (SessionStart, Setup only) |
CLAUDE_SESSION_ID | Current session identifier |
CLAUDE_CODE_REMOTE | "true" if running in web environment |
The CLAUDE_ENV_FILE enables SessionStart hooks to persist environment variables across the session.
Writing export VAR=value lines to this file makes them available to subsequent hooks.
Security model
Hooks execute with the user's full privileges. They can read any file, make network requests, modify system state. This power requires corresponding caution.
Claude Code captures hook configurations at session startup. If settings files change during a session, the changes do not take effect until the session restarts. This prevents configuration injection attacks where malicious code modifies hook settings mid-session.
When external changes to hook configuration are detected, Claude Code warns the user and requires explicit approval via the /hooks command before applying the new configuration.
Enterprise deployments can set allowManagedHooksOnly: true to block all user, project, and plugin hooks.
Only hooks from managed enterprise policies execute, ensuring centralized control over automation behavior.
The /hooks command provides an interactive interface for viewing and managing hook configuration.
Use it to verify which hooks are active and troubleshoot unexpected behavior.
Prompt-type hooks
Beyond shell commands, Claude Code supports prompt-based hooks that use an LLM to make decisions:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Evaluate if the task is complete: $ARGUMENTS",
"timeout": 30
}
]
}
]
}
}The $ARGUMENTS placeholder gets replaced with the current context.
If omitted, the input JSON is appended to the prompt instead.
A separate agent evaluates the prompt and returns:
{
"ok": true,
"reason": "All tests pass and documentation is updated"
}If ok is false and the event supports blocking, the operation is blocked.
Prompt-type hooks bridge deterministic automation with semantic evaluation. A Stop hook can check whether Claude actually finished the task rather than just stopped generating tokens.
Supported events for prompt-type hooks: Stop, SubagentStop, UserPromptSubmit, PreToolUse, PermissionRequest.
Codex comparison
Codex does not have a hooks system. This is the most significant automation gap between the two tools.
Codex offers a limited notification mechanism:
# config.toml
notify = ["python3", "/path/to/notify.py"]This fires only on agent-turn-complete events.
It cannot intercept operations, block tool calls, or modify inputs.
The notification receives event data but has no control over Codex behavior.
For automation that requires lifecycle control, Claude Code hooks are currently the only option among the two tools. Codex users who need similar functionality must implement external tooling or CI/CD pipelines to provide the missing guardrails.
The difference reflects architectural priorities. Claude Code treats the CLI as an extensible platform where users customize behavior. Codex treats the CLI as an interface to a cloud-based agent, with less emphasis on client-side programmability.
The next section covers practical hook patterns that leverage this system for real automation workflows.