How to Write and Configure Claude Code Hooks: A Complete Developer Guide
Claude Code hooks are executable scripts that intercept 25 built-in session events (like PreToolUse and UserPromptSubmit) via JSON stdin/stdout contracts, enabling automated validation, formatting, and security checks with configurable matchers and four execution types (command, HTTP, prompt, agent).
Claude Code hooks provide an event-driven automation layer that runs inside your Claude Code sessions. According to the luongnv89/claude-howto repository, these hooks allow you to enforce coding standards, block dangerous operations, and trigger custom workflows without leaving your editor. They execute at specific lifecycle moments, receiving contextual data via standard input and returning structured responses via standard output.
Understanding the Claude Code Hooks Architecture
The hook system in Claude Code operates through a structured event pipeline defined in 06-hooks/README.md. Understanding these core components is essential before writing your first script.
Event-Driven Execution Model
Claude Code exposes 25 built-in hook events that fire at well-defined moments in a session lifecycle. Critical events include:
PreToolUse– Fires before any tool execution (Bash, Write, Edit, etc.), allowing blocking or modificationPostToolUse– Fires after tool completion for logging or cleanupUserPromptSubmit– Intercepts user input before processing for content moderationStop– Triggers when a session ends for final validation or reportingSessionStartandPermissionRequest– Handle initialization and permission workflows
These events are documented in the hook events section of 06-hooks/README.md#hook-events.
The Four Hook Types
Hooks determine how scripts execute through four distinct types:
command– Executes a local bash or Python script with full shell accesshttp– POSTs JSON payloads to remote endpoints for external integrationsprompt– Evaluates an LLM prompt against the current context for intelligent decisionsagent– Spawns a sub-agent with tool access for complex multi-step validation
Each type serves different automation needs, from simple file formatting to complex security scanning.
Matcher Patterns for Targeted Execution
The matcher field controls which specific tool invocations trigger a hook. Patterns support:
- Exact strings:
"Bash"targets only Bash tool calls - Regex patterns:
"Write|Edit"matches multiple tool types - Wildcards:
"mcp__memory__.*"captures all MCP memory operations
Matchers are evaluated against the tool_name field in the incoming JSON payload, allowing surgical precision in hook deployment.
Configuration Scope and Precedence
Hooks can be declared across four configuration layers, with later layers overriding earlier ones:
- Global user settings:
~/.claude/settings.json(applies to all projects) - Project settings:
.claude/settings.json(repository-specific rules) - Plugin scope:
hooks/hooks.json(plugin-contained logic) - Component front-matter:
SKILL.md,agent.md, orcommand.mdfiles (task-specific hooks)
Writing Your First Claude Code Hook
All hooks must respect a strict input/output contract regardless of programming language.
The Input/Output Contract
Hooks receive a JSON blob via stdin containing session metadata, tool names, and inputs. They must return JSON on stdout with specific exit codes:
- Exit code
0: Success (non-blocking) or explicit allow - Exit code
2: Blocking error (prevents the original action from executing) - HTTP hooks: Must explicitly declare
allowedEnvVarsfor security
The payload includes fields like session_id, tool_name, tool_input, and event-specific context.
Example 1: Auto-Formatting After File Writes
The 06-hooks/format-code.sh script demonstrates a PostToolUse hook that automatically formats code after Write or Edit operations:
#!/bin/bash
# 06-hooks/format-code.sh
INPUT=$(cat) # read whole JSON payload
TOOL=$(echo "$INPUT" | python3 -c "import sys,json;print(json.load(sys.stdin).get('tool_name',''))")
FILE=$(echo "$INPUT" | python3 -c "import sys,json;print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))")
[[ "$TOOL" != "Write" && "$TOOL" != "Edit" ]] && exit 0
case "$FILE" in
*.js|*.jsx|*.ts|*.tsx|*.json) command -v prettier >/dev/null && prettier --write "$FILE" ;;
*.py) command -v black >/dev/null && black "$FILE" ;;
*.go) command -v gofmt >/dev/null && gofmt -w "$FILE" ;;
esac
exit 0
This script extracts the tool name and file path from stdin, checks if the operation modified a code file, and runs the appropriate formatter.
Example 2: Blocking Dangerous Commands
The 06-hooks/validate-prompt.py file shows a UserPromptSubmit hook that prevents destructive operations by analyzing user input:
#!/usr/bin/env python3
import json, sys, re
BLOCKED = [
(r"rm\s+-rf\s+/", "Dangerous: rm -rf /"),
(r"delete\s+database", "Dangerous: DB deletion"),
]
data = json.load(sys.stdin)
prompt = data.get("user_prompt") or data.get("prompt", "")
for pat, msg in BLOCKED:
if re.search(pat, prompt, re.I):
print(json.dumps({"decision":"block","reason":msg}))
sys.exit(0)
sys.exit(0)
When the regex matches dangerous patterns, the hook returns a JSON block decision and exits with code 0 (allowing the hook response to block the action), preventing data loss.
Example 3: LLM-Based Task Validation
For intelligent validation without custom code, use a prompt hook. This configuration snippet (from 06-hooks/README.md) runs when the session stops:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Did Claude complete all requested tasks? List any missing items.",
"timeout": 30
}
]
}
]
}
}
This leverages Claude's own reasoning to verify task completion before ending the session.
Configuring Hooks in Claude Code
Global vs. Project-Level Setup
Create a hooks directory and set executable permissions:
mkdir -p ~/.claude/hooks
cp 06-hooks/*.sh ~/.claude/hooks/
chmod +x ~/.claude/hooks/*.sh
For project-specific hooks, use .claude/hooks/ within your repository instead.
Complete Configuration Example
Add the following JSON structure to ~/.claude/settings.json or .claude/settings.json as shown in 06-hooks/README.md#complete-configuration-example:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py\"",
"timeout": 10
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/format-code.sh\"",
"timeout": 30
},
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/security-scan.py\"",
"timeout": 10
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-prompt.py\""
}
]
}
]
}
}
Restart Claude Code or run claude --debug to register the new hooks.
Debugging and Testing Your Hooks
Test hooks independently using sample JSON payloads:
echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"}}' | python3 .claude/hooks/validate-bash.py
echo $?
Enable verbose logging by pressing Ctrl+O inside Claude Code or starting with the --debug flag. The debug output shows hook execution order under "Running hooks for..." messages, helping you verify that matchers trigger correctly and exit codes propagate as expected.
Security Best Practices for Hook Development
Hooks execute with the same permissions as the Claude Code process. Follow these guidelines from 06-hooks/README.md#security-considerations:
- Validate all external input – Never trust
tool_inputfields without sanitization - Quote shell variables – Use
"$VAR"syntax to prevent word-splitting attacks - Use
$CLAUDE_PROJECT_DIR– Reference files through this environment variable instead of hardcoding absolute paths - Restrict environment variables – For HTTP hooks, explicitly list only required variables in
allowedEnvVars - Exclude secrets – Ensure hooks never read
.env,.git/, or private key files unless specifically designed for credential management
Summary
- Claude Code hooks intercept 25 session events via JSON stdin/stdout contracts, enabling event-driven automation
- Four hook types (command, HTTP, prompt, agent) support diverse execution models from shell scripts to LLM evaluation
- Matcher patterns (exact, regex, wildcard) provide surgical control over which tool invocations trigger hooks
- Configuration layers range from global
~/.claude/settings.jsonto project-specific.claude/settings.jsonand component front-matter - Exit code
2blocks operations forPreToolUseandUserPromptSubmitevents, while code0allows continuation - Security requires input validation, quoted variables, and
$CLAUDE_PROJECT_DIRusage to prevent path traversal and injection attacks
Frequently Asked Questions
How do I block a dangerous command before it executes?
Create a PreToolUse hook with a matcher targeting the specific tool (e.g., "Bash"), then implement validation logic that inspects tool_input.command from the JSON stdin. Return exit code 2 or output {"decision":"block","reason":"..."} to prevent execution. The 06-hooks/validate-prompt.py example demonstrates pattern matching for dangerous commands.
Can I use Claude Code hooks to automatically format code after every file save?
Yes. Configure a PostToolUse hook with the matcher "Write|Edit" that executes a command hook script. The 06-hooks/format-code.sh reference implementation reads the file path from stdin and runs prettier, black, or gofmt based on file extension. Set the timeout appropriately (30 seconds recommended) to prevent blocking on large files.
What is the difference between command hooks and prompt hooks?
Command hooks execute local shell or Python scripts with full access to the filesystem and environment, suitable for formatting, linting, or file system operations. Prompt hooks send a text prompt to the LLM for evaluation and return the response, ideal for semantic analysis, task completion verification, or content moderation without writing custom parsing code.
Where should I store my hook scripts for team-wide access?
Store hooks in the .claude/hooks/ directory within your project repository and reference them using "$CLAUDE_PROJECT_DIR/.claude/hooks/script-name" in your .claude/settings.json. This ensures all team members use identical validation logic when working in the same repository, while global hooks in ~/.claude/hooks/ remain specific to individual developer preferences.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →