Hooks
Deterministic automation that runs before or after Claude's tool use.
Hooks vs. CLAUDE.md
This is the key distinction:
- CLAUDE.md — Advisory. Claude reads it and tries to follow it. It may forget or misjudge.
- Hooks — Deterministic. Your script runs automatically every time, no matter what. Claude cannot skip it.
Use CLAUDE.md for guidelines and preferences. Use hooks for rules that must never be violated.
Hook Events
Hooks fire at specific points in Claude's agent loop. The most commonly used events:
| Event | When it fires |
|---|---|
PreToolUse | Before a tool executes — can approve, deny, or modify the call |
PostToolUse | After a tool completes successfully |
PostToolUseFailure | After a tool execution fails |
UserPromptSubmit | When the user submits a prompt, before Claude processes it |
Stop | When the main agent finishes responding |
SubagentStart / SubagentStop | When a subagent starts or completes |
TaskCreated / TaskCompleted | When an agent-team task is created or completed |
TeammateIdle | When an agent-team teammate becomes idle |
SessionStart / SessionEnd | At the beginning or end of a session |
PreCompact / PostCompact | Before and after context compaction |
Notification | Permission prompts, idle alerts, auth events |
PermissionDenied | After auto-mode classifier denials (return {retry: true} to let Claude retry) |
Configuration
Hooks are defined in .claude/settings.json under the hooks key. Each event maps to an array of matcher objects, and each matcher contains a hooks array with one or more handler definitions:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx eslint --fix"
}
]
}
]
}
}The matcher is a regex tested against the tool name (case-sensitive). Use | to match multiple tools: "Write|Edit". The type field picks the handler: "command" runs a shell script, "http" POSTs the event JSON to an endpoint, "mcp_tool" calls a connected MCP tool, "prompt" evaluates with a Claude model, "agent" spawns a subagent.
Use command hooks for fast deterministic checks. Use prompt or agent hooks only when the check genuinely needs model judgment; they add latency and token cost.
Practical Examples
Auto-Lint After Every Write
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx eslint --fix"
}
]
}
]
}
}Block Writes to Sensitive Files
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | grep -qE '(\\.env|secrets|credentials)' && exit 2 || exit 0"
}
]
}
]
}
}If the hook exits with code 2, Claude Code blocks the tool and feeds the error message back to Claude. Exit code 0 means success; other non-zero codes are non-blocking errors.
Run Tests After Changes
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "file=$(jq -r '.tool_input.file_path'); if echo \"$file\" | grep -q 'src/'; then npm test -- --related \"$file\"; fi"
}
]
}
]
}
}Run a Script When Claude Finishes
{
"hooks": {
"Stop": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude finished\" with title \"Claude Code\"'"
}
]
}
]
}
}Hook Input & Environment Variables
Hook scripts receive the full event context as JSON on stdin — parse it with jq or similar. Key fields:
tool_name— the tool that was called (e.g.,Write,Bash,Read)tool_input— the tool's arguments object; for Write/Edit/Read,tool_input.file_pathis always an absolute pathtool_response— (PostToolUse only) the result returned by the toolsession_id,cwd,transcript_path
Environment variables exported to hook commands:
$CLAUDE_PROJECT_DIR— project root$CLAUDE_PLUGIN_ROOT/$CLAUDE_PLUGIN_DATA— set inside plugin-defined hooks$CLAUDE_ENV_FILE— (SessionStart / Setup / CwdChanged / FileChanged only) appendexport VAR=valuelines here to persist env vars into subsequent Bash calls$CLAUDE_EFFORT— current effort level (inside tool-use context)$CLAUDE_CODE_REMOTE—"true"when running in a remote web environment
File paths and
session_idare only in the stdin JSON, not env vars. Parse withjq -r '.tool_input.file_path'rather than reaching for a non-existent$CLAUDE_TOOL_INPUT_FILE_PATH.
Conditional Hooks with if
Add an if field to a handler (inside the inner hooks[] array, not on the outer matcher) to fire only when a permission rule matches. This reduces unnecessary process spawning:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "echo 'Git command detected' >> ~/.claude/audit.log"
}
]
}
]
}
}Disabling Hooks
To disable all hooks for a session (useful for debugging):
{
"disableAllHooks": true
}Hooks are deterministic control over an agent
The advisory vs. deterministic distinction is a core concept in agent design. Hooks give you guaranteed behavior regardless of what the LLM decides — a pattern used in all production agent systems.
Related Reading
- CLAUDE.md & Memory — Advisory context (complements hooks)
- Skills — Package reusable workflows
- Subagents — Delegate focused work
- Agent Teams — Coordinate team lifecycle events
- Automation — Pair hooks with goals, loops, and routines
- Workflows — Use hooks in development workflows
Continue with practice
You have finished the core ideas of Hooks.
If you want to turn the idea into something reusable, continue practicing on AgentWay.