Claude Code Hooks are user-defined shell commands that run automatically at specific points in the Claude Code lifecycle. Rather than hoping the AI "decides" to do something, hooks guarantee it happens every single time. That shift from probabilistic to deterministic control is what makes hooks so powerful for production workflows.
The March 2026 update brought major expansions: MCP elicitation support, StopFailure for hard blocks, transcript search, additional hook events, subprocess credential scrubbing, and more. With these additions, hooks have matured into a full-featured automation layer that belongs in every serious Claude Code setup.
The Core Concept — Why Deterministic Control Matters
When Claude Code works autonomously—writing files, running commands, editing code—it's remarkably capable. But capability isn't the same as predictability. "Please always run the formatter" works most of the time, but "most of the time" isn't good enough when you're protecting production databases or enforcing team coding standards.
Hooks solve this by intercepting actions at precisely the right moment. They're lifecycle event listeners that let you attach custom logic to Claude Code's execution pipeline. The AI still makes creative decisions about what code to write, but the rules around that code—formatting, validation, security checks—are enforced mechanically.
Think of it this way: the AI handles the "what" while hooks handle the "always." That combination of creativity and reliability is what makes Claude Code genuinely production-ready.
Understanding the Four Hook Types
Claude Code Hooks are organized into four types, each corresponding to a different phase of the execution lifecycle.
PreToolUse — Intercepting Before Execution
PreToolUse fires just before a tool runs. It acts as a gatekeeper: you can block dangerous operations, validate inputs, or inject warnings. The hook receives the tool name and input as environment variables, and its exit code determines what happens next.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "python3 /scripts/validate-command.py \"$TOOL_INPUT\"",
"description": "Block dangerous command execution"
}
]
}
}If the hook exits with code 0, execution proceeds normally. Any stdout output gets passed to Claude as context. Exit code 2 triggers a StopFailure, which we'll cover in detail later.
PostToolUse — Automating After Completion
PostToolUse fires immediately after a tool completes successfully. This is where you put automatic formatting, linting, test execution, notifications—anything that should happen as a consequence of a tool's action.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "npx prettier --write \"$FILEPATH\" 2>/dev/null || true",
"description": "Auto-format files after editing"
}
]
}
}SessionStart — Initializing Context
SessionStart fires when a Claude Code session begins. It's perfect for injecting project-specific context, loading team conventions, or setting up environment variables that Claude should know about.
{
"hooks": {
"SessionStart": [
{
"command": "cat /project/.context/team-rules.md",
"description": "Auto-load project rules on session start"
}
]
}
}Stop — Wrapping Up
Stop fires when Claude finishes a response or the session ends. Use it for logging, cleanup, or sending notifications when Claude needs human input.
{
"hooks": {
"Stop": [
{
"command": "echo \"[$(date)] Session ended\" >> /project/.logs/claude-sessions.log",
"description": "Log session completion"
}
]
}
}The Complete Event Catalog — All 18 Events
The March 2026 update expanded the event catalog to 18 distinct events. You don't need to memorize all of them, but understanding the categories helps you quickly find the right hook for your use case.
File Operation Events (5)
Write, Edit, MultiEdit, FileRead, and DirectoryRead. These are the most frequently used events for quality control. Write and Edit are essential for auto-formatting and linting; FileRead and DirectoryRead are useful for access control and audit logging.
Command Execution Events (3)
Bash, BashStreaming, and SubProcess. Bash is the workhorse for blocking dangerous commands. SubProcess, added in March 2026, fires during subprocess creation and supports credential scrubbing to prevent secret leakage in logs.
Search and Analysis Events (4)
Grep, Glob, WebSearch, and TranscriptSearch. TranscriptSearch is a new March 2026 addition that fires when searching past session transcripts. You can use it to filter results or log search patterns for team insights.
Communication Events (3)
UserMessage, AssistantMessage, and MCPElicitation. MCPElicitation handles additional information requests from MCP servers, giving you control over how external tools interact with Claude during a session.
Session Management Events (3)
SessionStart, SessionResume, and Stop. SessionResume fires when a paused session is resumed, letting you automatically restore the previous working context. This is particularly useful for long-running development tasks that span multiple sittings.
Configuration Files and Priority
Hooks are defined in JSON and can be set at three levels, with a clear priority hierarchy.
Project Level (.claude/settings.json)
This lives in your project root and gets committed to Git, ensuring every team member runs the same hooks. It's the right place for formatting rules, security blocks, and CI integration.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "bash /scripts/block-dangerous-commands.sh \"$TOOL_INPUT\"",
"description": "Prevent direct production operations"
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "npx eslint --fix \"$FILEPATH\" 2>/dev/null; npx prettier --write \"$FILEPATH\" 2>/dev/null || true",
"description": "Auto-run ESLint + Prettier"
}
]
}
}User Level (~/.claude/settings.json)
Personal preferences that don't need to be shared: notification endpoints, local tool paths, personal productivity scripts.
Session Level
Temporary overrides for a specific session—useful for enabling verbose logging during a debugging session, for example.
Priority flows from most specific to least: Session > User > Project. When multiple hooks are defined for the same event, they all execute in sequence. However, a single StopFailure in a PreToolUse chain halts the entire tool execution immediately.
Five Production-Ready Automation Recipes
Let's get practical. These are battle-tested patterns that deliver immediate value in real projects.
Recipe 1: Auto-Format and Lint on Every File Change
The single most impactful automation. Every file Claude edits gets automatically formatted and linted, with language-appropriate tools selected by file extension.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"command": "bash -c 'FILE=\"$FILEPATH\"; EXT=\"${FILE##*.}\"; case \"$EXT\" in ts|tsx|js|jsx) npx prettier --write \"$FILE\" && npx eslint --fix \"$FILE\" 2>/dev/null;; py) black \"$FILE\" 2>/dev/null && ruff check --fix \"$FILE\" 2>/dev/null;; rs) rustfmt \"$FILE\" 2>/dev/null;; esac; exit 0'",
"description": "Language-aware auto-formatting"
}
]
}
}The key insight is matching the formatter to the file type. TypeScript gets Prettier plus ESLint, Python gets Black plus Ruff, Rust gets rustfmt. The exit 0 at the end ensures formatting errors don't block Claude's workflow.
Recipe 2: Dangerous Command Blocking
A critical safety net for any team that values their production environment.
#!/bin/bash
# /scripts/block-dangerous-commands.sh
INPUT="$1"
# Define dangerous patterns
DANGEROUS_PATTERNS=(
"rm -rf /"
"git push.*--force"
"git push.*-f"
"DROP TABLE"
"DROP DATABASE"
"chmod 777"
"> /dev/sda"
"mkfs"
":(){:|:&};:"
)
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if echo "$INPUT" | grep -qiE "$pattern"; then
echo "BLOCKED: Dangerous command detected: $pattern"
echo "This command has been blocked for safety."
exit 2 # Trigger StopFailure
fi
done
exit 0Exit code 2 triggers StopFailure, which completely prevents the tool from executing. Exit code 1 logs an error but may allow continued execution. Exit code 2 is an explicit "no, absolutely not."
Recipe 3: Slack and Discord Notifications
Keep developers in the loop without requiring them to watch the terminal. Send a notification when Claude needs input or finishes a long task.
{
"hooks": {
"Stop": [
{
"command": "bash -c 'if [ \"$STOP_REASON\" = \"needs_input\" ]; then curl -s -X POST \"$SLACK_WEBHOOK_URL\" -H \"Content-Type: application/json\" -d \"{\\\"text\\\": \\\"Claude Code is waiting for input: $SESSION_NAME\\\"}\"; fi'",
"description": "Notify Slack when waiting for input"
}
]
}
}Recipe 4: Automatic Context Injection at Session Start
Load project conventions, recent changes, and team guidelines automatically so Claude starts every session with full context.
{
"hooks": {
"SessionStart": [
{
"command": "bash -c 'echo \"=== Project Context ===\"; cat .claude/CONTEXT.md 2>/dev/null; echo \"\\n=== Recent Changes ===\"; git log --oneline -10 2>/dev/null; echo \"\\n=== Active Branch ===\"; git branch --show-current 2>/dev/null'",
"description": "Auto-inject project context"
}
]
}
}Recipe 5: CI Pipeline Integration
Automatically run relevant tests after file changes. Test failures get fed back to Claude, creating a tight quality feedback loop.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "bash -c 'FILE=\"$FILEPATH\"; if [[ \"$FILE\" == *.test.* ]] || [[ \"$FILE\" == *__tests__* ]]; then npx jest --testPathPattern=\"$FILE\" --no-coverage 2>&1 | tail -20; elif [[ \"$FILE\" == *.ts ]] || [[ \"$FILE\" == *.tsx ]]; then npx tsc --noEmit 2>&1 | head -20; fi; exit 0'",
"description": "Auto-run tests on test file changes"
}
]
}
}StopFailure — The Hard Block You Need
StopFailure, introduced in March 2026, is arguably the most important safety feature in the hooks system. When a PreToolUse hook returns exit code 2, the tool execution is immediately and unconditionally halted. Claude receives the error message as feedback and can suggest alternatives.
Before StopFailure, the only options were exit 0 (proceed) and exit 1 (error, but potentially continue). This left a gap: there was no way to absolutely guarantee that a dangerous operation would be prevented. StopFailure closes that gap.
#!/bin/bash
# StopFailure usage patterns
# exit 0 — Success. Tool execution proceeds
# exit 1 — Error logged, but execution may continue
# exit 2 — StopFailure. Tool execution halted immediately
# Example: Block production database connections
if echo "$TOOL_INPUT" | grep -q "production-db"; then
echo "ERROR: Direct connections to production database are blocked."
echo "Please use the staging environment (staging-db) instead."
exit 2 # StopFailure — absolutely no execution
fi
# Example: Warn about deprecated APIs (but don't block)
if echo "$TOOL_INPUT" | grep -q "deprecated-api"; then
echo "WARNING: This API is deprecated. Consider migrating to the new API."
exit 0 # Warning only. Execution continues
fiA best practice: always include a specific, actionable reason in your StopFailure messages. Claude reads the error output and uses it to suggest alternatives, which creates a productive feedback loop for the developer.
MCP Elicitation Integration
MCP elicitation support means hooks can now control the interaction flow between Claude and external MCP servers. When an MCP server requests additional information, the MCPElicitation event fires, and your hook can intercept and respond.
A practical example: automatically providing authentication credentials when an MCP server requests them, pulling from environment variables or a secret manager rather than prompting the user.
{
"hooks": {
"PreToolUse": [
{
"matcher": "MCPElicitation",
"command": "bash -c 'if echo \"$TOOL_INPUT\" | grep -q \"auth_required\"; then echo \"{\\\"token\\\": \\\"$(cat ~/.secrets/mcp-token)\\\"}\"; fi'",
"description": "Auto-respond to MCP authentication requests"
}
]
}
}Combined with the SubProcess event, you can also ensure that credentials are scrubbed from subprocess logs—an essential security measure for teams handling sensitive data.
Operational Best Practices and Troubleshooting
A few important considerations for running hooks in production.
Testing Your Hooks
Hook commands are regular shell scripts, so you can test them independently. Mock the environment variables ($TOOL_INPUT, $FILEPATH, $STOP_REASON) and verify the expected exit codes and output.
# Test a blocking hook
TOOL_INPUT='{"command": "rm -rf /"}' bash /scripts/block-dangerous-commands.sh "$TOOL_INPUT"
echo "Exit code: $?" # Should be 2 for a successful blockPerformance Considerations
PostToolUse hooks run frequently, so keep them fast. Heavy operations should run in the background or target only changed files. A slow formatter that adds 3 seconds to every edit will quickly become frustrating.
Debugging Tips
If a hook isn't firing as expected, check the matcher first. Matchers use pipe-delimited string matching, not regex. Write|Edit means "Write or Edit," but Write.* won't work—it's not a regular expression.
Team Governance
Project-level hooks affect everyone on the team, so treat .claude/settings.json like any other piece of shared infrastructure. Use pull requests for changes, require code review, and document the purpose of each hook clearly.
Wrapping Up
Claude Code Hooks bridge the gap between AI flexibility and deterministic reliability. With four hook types and eighteen events at your disposal, you can automate virtually any aspect of your development workflow while maintaining strict quality and security guarantees.
Start with the highest-impact automations—auto-formatting and dangerous command blocking—then gradually add project-specific hooks as your needs evolve. Once you've experienced the peace of mind that comes from deterministic control, you won't want to go back to hoping the AI remembers to run the formatter.