Skip to main content Show chat and tutor
Tracks / Claude Code / Hooks: Deterministic Automation Hooks: Deterministic Automation
Hooks let Claude Code react to events — auto-format on edit, run tests on save, validate before commit. Combined with subagents, they turn Claude from an assistant into an autonomous engineer.
Wire hooks to fire on Claude Code events — PreToolUse, PostToolUse, SessionStart, SessionEnd, CompactBefore, CompactAfter, PermissionRequest, FileWatcherChange Pick the right hook type — shell command, HTTP webhook, prompt-based, or agent-based — for any automation Use the `if` field (2026) to conditionally fire hooks, reducing overhead on hot paths Recognize when hooks beat instructions in CLAUDE.md (deterministic enforcement vs hopeful suggestion)
Why This Matters
Hooks turn 'please remember to format' into 'always formats' deterministically. You stop hoping Claude follows the rule and the runtime enforces it
Production teams use hooks for : auto-format on edit, lint after writes, secret scanning before commit, Slack notification on session end, audit log of every Bash command
The 2026 update : conditional if field cuts unnecessary hook executions; better error handling; agent-based hooks let one Claude session call another
Build this → Add a PostToolUse hook that runs prettier --write on every file Claude edits; verify formatting happens automatically without you asking
# How Hooks Think
Intuition
Build Your Intuition Hooks turn Claude Code from a polite assistant into an enforced system. CLAUDE.md is a memo ("please format Python files after editing"); a hook is the formatter actually running. Memos can be forgotten; hooks fire 100% of the time.
What’s one thing you learned? What’s still confusing?
How was the difficulty? Easy Just Right Hard
Continue Learning Claude Code Mastery
Subagents & Delegation
Define reusable specialized agents in .claude/agents/. Sub-agent vs fork, common recipes (code-reviewer, docs-writer, test-generator).
Claude Code Mastery
MCP: External Connections
Register MCP servers (GitHub, Slack, Postgres, Linear, Figma...) — auth patterns, /mcp command, alwaysLoad option, concurrent connections.
Claude Code Mastery
Code Generation & Feature Building
Build features, APIs, components, and tests from natural language. The vertical slice workflow.
Discussion Ask questions, share insights
See AI think. Learn by watching machines learn — interactive ML visualizations from linear algebra to agents.
16 learning paths314 lessons
© 2026 RugvAI Labs. All rights reserved.
Interactive visualizations powered by D3.js · Three.js
Mental model: every action Claude takes — reading a file, running a Bash command, ending a session — is an event . Hooks subscribe to events and run code in response. It's the same pattern as DOM event listeners, webhook subscriptions, or pre-commit git hooks. If you've used any of those, you already understand the model.
You add a PreToolUse hook that runs eslint --max-warnings 0 on the entire repo before every Bash tool call. What is the first thing that breaks?
Nothing — ESLint is fast on modern hardware The hook silently never fires Every Bash invocation — even `ls` or `pwd` — now waits 10-30 seconds for ESLint to scan the whole repo, making the session feel broken Claude Code throws a permissions error
Lock In My Prediction
Event Fires when Common use PreToolUse Before any tool call Block dangerous operations, log audit PostToolUse After any tool call succeeds Auto-format edited files, run linters SessionStart New Claude session opens Load project state, post Slack notification SessionEnd Session closes Save state, post summary, update tracker CompactBefore Auto-compaction about to happen Save important context to memory CompactAfter Auto-compaction completed Verify nothing important was lost PermissionRequest User about to be asked for permission Auto-decide (e.g., always block in CI) FileWatcherChange Watched file changed (2026) React to external editor changes
In .claude/settings.json:
json {
"hooks": [
{
"event": "PostToolUse",
"matcher": { "tool": "Edit" },
"command": "npx prettier --write \"$CLAUDE_TOOL_FILE_PATH\""
},
{
"event": "PreToolUse",
"matcher": { "tool": "Bash", "pattern": "git push.*--force.*(main|master)" },
"command": "echo 'BLOCKED: never force-push to main' && exit 1"
},
{
"event": "SessionEnd",
"command": "curl -X POST $SLACK_WEBHOOK -d '{\"text\":\"Claude session ended in $(pwd)\"}'"
}
]
}
# 1. Shell Command (most common)json {
"event": "PostToolUse",
"matcher": { "tool": "Edit" },
"command": "npx prettier --write \"$CLAUDE_TOOL_FILE_PATH\""
}
json {
"event": "SessionEnd",
"url": "https://hooks.slack.com/services/...",
"method": "POST",
"body": { "text": "Session ended", "session_id": "${CLAUDE_SESSION_ID}" }
}
# 3. Prompt-Based (have Claude evaluate something)json {
"event": "PostToolUse",
"matcher": { "tool": "Write", "pattern": "\\.env|secrets" },
"prompt": "Did the recent Write include any secrets, API keys, or PII? Reply YES or NO only."
}
If the prompt returns YES, the action is blocked.
# 4. Agent-Based (delegate to a sub-agent)json {
"event": "PostToolUse",
"matcher": { "tool": "Edit", "pattern": "src/components/.*\\.tsx" },
"agent": "ui-reviewer"
}
This fires the .claude/agents/ui-reviewer.yaml agent on the changed file. (See Subagents lesson.)
Conditional Hooks (if field, 2026) Avoid running hooks on every tool use — only when relevant:
json {
"event": "PostToolUse",
"matcher": { "tool": "Edit" },
"if": "endswith($CLAUDE_TOOL_FILE_PATH, '.py')",
"command": "ruff format \"$CLAUDE_TOOL_FILE_PATH\""
},
{
"event": "PostToolUse",
"matcher": { "tool": "Edit" },
"if": "endswith($CLAUDE_TOOL_FILE_PATH, '.tsx') || endswith($CLAUDE_TOOL_FILE_PATH, '.ts')",
"command": "npx prettier --write \"$CLAUDE_TOOL_FILE_PATH\""
}
Without if, both hooks fire on every Edit and one fails (because ruff doesn't apply to .tsx). With if, each hook only runs when relevant.
# Environment Variables in HooksVariable Provides $CLAUDE_TOOL_NAMEThe tool that was used (Read, Edit, Bash, ...) $CLAUDE_TOOL_FILE_PATHThe file path (for Read/Edit/Write) $CLAUDE_TOOL_INPUTThe full tool input as JSON $CLAUDE_TOOL_OUTPUTThe tool's output (PostToolUse only) $CLAUDE_SESSION_IDThe current session UUID $CLAUDE_PROJECT_DIRProject root directory $USERCurrent user (from OS)
# Hooks vs Instructions in CLAUDE.mdNeed Hooks CLAUDE.md "Always format after edits" ✅ deterministic ⚠️ Claude might forget "Don't force-push to main" ✅ blocks at runtime ⚠️ Claude might do it once "Use kebab-case filenames" ⚠️ hard to enforce mechanically ✅ guides decision "When user asks vague question, propose plan first" ❌ wrong layer ✅ guides behavior
Rule : if you can write a deterministic check, use a hook. If it's a judgment or convention, use CLAUDE.md.
json {
"event": "PostToolUse",
"matcher": { "tool": "Edit" },
"if": "endswith($CLAUDE_TOOL_FILE_PATH, '.tsx') || endswith($CLAUDE_TOOL_FILE_PATH, '.ts')",
"command": "npx prettier --write \"$CLAUDE_TOOL_FILE_PATH\""
}
json {
"event": "PreToolUse",
"matcher": { "tool": "Bash", "pattern": "git commit" },
"command": "git diff --cached | grep -E '(API_KEY|SECRET|PRIVATE_KEY)' && exit 1 || exit 0"
}
json {
"event": "PostToolUse",
"matcher": { "tool": "Write" },
"if": "endswith($CLAUDE_TOOL_FILE_PATH, '.py')",
"command": "ruff check \"$CLAUDE_TOOL_FILE_PATH\" --fix"
}
# UserPromptSubmit guard (block off-policy asks)json {
"event": "UserPromptSubmit",
"if": "contains($CLAUDE_USER_PROMPT, 'delete production')",
"command": "echo 'BLOCKED: production destructive intent. File a ticket instead.' && exit 1"
}
UserPromptSubmit fires before the model even sees the message — useful for company policy enforcement (no production destructive ops from interactive sessions, no PII in prompts, etc.).
# Stop hook (post-session digest)json {
"event": "Stop",
"command": "echo \"$(date -u +%FT%TZ) session=$CLAUDE_SESSION_ID files=$(git diff --name-only | wc -l)\" >> ~/.claude/session-log.tsv"
}
Stop fires when Claude finishes a turn (good for digests). SessionEnd fires when the user exits the whole session.
# Audit every Bash commandjson {
"event": "PreToolUse",
"matcher": { "tool": "Bash" },
"command": "echo \"$(date -u +%FT%TZ) [$USER] $(echo $CLAUDE_TOOL_INPUT | jq -r .command)\" >> ~/.claude/bash-audit.log"
}
Hooks are deterministic enforcement. Turn "please remember to format" into "always formats"
8 events cover the lifecycle: PreToolUse, PostToolUse, SessionStart/End, CompactBefore/After, PermissionRequest, FileWatcherChange
4 hook types : shell, HTTP, prompt-based, agent-based — pick by what the hook does
Conditional if field (2026) prevents hot-path overhead
CLAUDE.md vs hooks : judgment goes in CLAUDE.md, mechanical checks go in hooks
Quick Check 1 / 2
Your team keeps forgetting to run `ruff format` after Python edits. Best fix?
A Add 'always run ruff format' to CLAUDE.md and hope B Use eslint instead C Switch to a different formatter D Add a PostToolUse hook with `if: endswith(file, '.py')` that runs `ruff format`
Check Answer
Quick check
You want every `Edit` to a `.py` file to be auto-formatted with `ruff`, but only that — no overhead on `.ts` edits. Which event + scoping is correct?
A PostToolUse matcher.tool=Edit, `if: endswith($CLAUDE_TOOL_FILE_PATH, '.py')`, command: `ruff format ...` B PreToolUse matcher.tool=Edit, no `if` C SessionEnd, command: `ruff format .` D UserPromptSubmit, command: `ruff format ...`
Nice work! You just learned how to wire deterministic event-driven automation around every Claude Code action.
Progress 8 of 17 lessons to Python fluency
Up next: sub-agents — parallel specialists with isolated context
Hooks enforce rules deterministically. Next: subagents — when you need a separate context for a specialized task.