PreToolUse
Before tool execution - Validate, block, or modify tool calls before they run. Perfect for security checks and input validation.
Ta treść nie jest jeszcze dostępna w Twoim języku.
Claude Code’s hooks system (introduced July 2025) enables powerful event-driven automation. By executing custom commands at specific points in Claude’s workflow, you can enforce standards, automate quality checks, and create sophisticated development workflows - all without manual intervention.
Hooks are shell commands that execute automatically when specific events occur in Claude Code. Think of them as lifecycle callbacks that let you inject custom behavior into Claude’s operations.
PreToolUse
Before tool execution - Validate, block, or modify tool calls before they run. Perfect for security checks and input validation.
PostToolUse
After tool completion - React to successful operations. Ideal for formatting, notifications, and follow-up actions.
UserPromptSubmit
When user submits prompt - Process or enhance prompts before Claude sees them. Add context or validate requests.
Stop
When Claude finishes - Cleanup, reporting, or triggering next steps after Claude completes its response.
Additional events:
Open hooks configuration
/hooks# Select: PreToolUse
Add a matcher for Edit tool
Matcher: Edit
Add formatting hook
prettier --write "$CLAUDE_FILE_PATHS" 2>/dev/null || true
Save to project settings
Choose .claude/settings.json
for team sharing
Now every file Claude edits will be automatically formatted!
// .claude/settings.json (shared with team){ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "prettier --write \"$CLAUDE_FILE_PATHS\"", "timeout": 5000 } ] } ], "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "/path/to/security-check.sh" } ] } ] }}
// ~/.claude/settings.json (personal){ "hooks": { "Notification": [ { "hooks": [ { "type": "command", "command": "osascript -e 'display notification \"Claude needs attention\" with title \"Claude Code\"'" } ] } ] }}
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "/opt/security/audit-command.sh" } ] } ] }}
Matchers determine which tools trigger your hooks:
Pattern | Description | Example |
---|---|---|
Edit | Exact match | Only Edit tool |
Edit|Write | Multiple tools | Edit OR Write |
Notebook.* | Regex pattern | NotebookEdit, NotebookRead |
"" or omitted | All tools | Every tool call |
mcp__.*__.* | MCP tools | All MCP server tools |
Hooks receive JSON via stdin:
{ "session_id": "abc123", "transcript_path": "/path/to/conversation.jsonl", "cwd": "/current/working/directory", "hook_event_name": "PreToolUse", "tool_name": "Edit", "tool_input": { "file_path": "/src/index.js", "content": "// File content here" }}
Hooks communicate through exit codes and output:
Exit Code | Effect | Use Case |
---|---|---|
0 | Success, continue | Normal flow |
2 | Block with feedback | Validation failed |
Other | Non-blocking error | Warning only |
#!/bin/bashif [[ "$1" =~ rm\ -rf ]]; then echo "Dangerous command blocked" >&2 exit 2fiexit 0
#!/bin/bashINPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
if [[ "$TOOL_NAME" == "Edit" ]]; then cat <<EOF{ "decision": "approve", "reason": "Edit approved after validation", "suppressOutput": true}EOFelse cat <<EOF{ "decision": "block", "reason": "Only Edit tool allowed in this context"}EOFfi
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "sh -c 'if [[ \"$CLAUDE_FILE_PATHS\" =~ \\.(js|ts|jsx|tsx)$ ]]; then prettier --write \"$CLAUDE_FILE_PATHS\" && eslint --fix \"$CLAUDE_FILE_PATHS\"; fi'" } ] }, { "matcher": "Edit", "hooks": [ { "type": "command", "command": "sh -c 'if [[ \"$CLAUDE_FILE_PATHS\" =~ \\.(py)$ ]]; then black \"$CLAUDE_FILE_PATHS\" && ruff check --fix \"$CLAUDE_FILE_PATHS\"; fi'" } ] } ] }}
#!/bin/bashset -e
INPUT=$(cat)FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
# Prevent editing sensitive filesFORBIDDEN_PATTERNS=( "*.env" "*.key" "*.pem" "**/secrets/*" "**/config/production.json")
for pattern in "${FORBIDDEN_PATTERNS[@]}"; do if [[ "$FILE_PATH" == $pattern ]]; then echo "Security policy prevents editing $FILE_PATH" >&2 exit 2 fidone
# Check for secrets in contentCONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty')if echo "$CONTENT" | grep -qE '(api_key|password|secret).*=.*["\x27]'; then echo "Potential secret detected in code" >&2 exit 2fi
exit 0
#!/bin/bash# Comprehensive TypeScript workflow
set -eINPUT=$(cat)FILES=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
for FILE in $FILES; do if [[ "$FILE" =~ \.(ts|tsx)$ ]]; then # Format prettier --write "$FILE"
# Type check npx tsc --noEmit --skipLibCheck "$FILE" || { echo "TypeScript errors in $FILE - please review" # Don't fail, just warn }
# Update tests if needed TEST_FILE="${FILE%.ts}.test.ts" if [[ -f "$TEST_FILE" ]]; then echo "Remember to update tests: $TEST_FILE" fi
# Generate/update documentation npx typedoc --emit none --validation "$FILE" 2>/dev/null || true fidone
#!/bin/bash# Platform-aware notifications
INPUT=$(cat)MESSAGE=$(echo "$INPUT" | jq -r '.message // "Claude needs attention"')
# macOSif command -v osascript &> /dev/null; then osascript -e "display notification \"$MESSAGE\" with title \"Claude Code\" sound name \"Glass\""
# Linux with notify-sendelif command -v notify-send &> /dev/null; then notify-send "Claude Code" "$MESSAGE" --urgency=normal --icon=dialog-information
# Windows WSLelif command -v powershell.exe &> /dev/null; then powershell.exe -Command " Add-Type -AssemblyName System.Windows.Forms \$notification = New-Object System.Windows.Forms.NotifyIcon \$notification.Icon = [System.Drawing.SystemIcons]::Information \$notification.BalloonTipIcon = 'Info' \$notification.BalloonTipTitle = 'Claude Code' \$notification.BalloonTipText = '$MESSAGE' \$notification.Visible = \$true \$notification.ShowBalloonTip(5000) "fi
# Also log to fileecho "[$(date)] $MESSAGE" >> ~/.claude/notifications.log
#!/bin/bash# Add context to user prompts
INPUT=$(cat)PROMPT=$(echo "$INPUT" | jq -r '.prompt')
# Add git contextGIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "no-git")GIT_STATUS=$(git status --porcelain | wc -l)
# Add system contextNODE_VERSION=$(node -v 2>/dev/null || echo "no-node")
# Output enhanced promptcat <<EOF{ "prompt": "$PROMPT\n\nContext: Branch=$GIT_BRANCH, Uncommitted=$GIT_STATUS files, Node=$NODE_VERSION", "continue": true}EOF
{ "hooks": { "PostToolUse": [ { "matcher": "Edit", "hooks": [ { "type": "command", "command": "sh -c 'if [ \"$CI\" = \"true\" ]; then echo \"Skipping format in CI\"; else prettier --write \"$CLAUDE_FILE_PATHS\"; fi'" } ] } ] }}
{ "hooks": { "PreToolUse": [ { "matcher": "mcp__database__.*", "hooks": [ { "type": "command", "command": "/opt/audit/log-database-access.sh" } ] } ], "PostToolUse": [ { "matcher": "mcp__github__create_pr", "hooks": [ { "type": "command", "command": "slack-cli send --channel #dev-prs --text 'New PR created by Claude'" } ] } ] }}
#!/bin/bash# Robust hook template
set -euo pipefailtrap 'echo "Hook failed: $?" >&2' ERR
# Safe JSON parsingINPUT=$(cat)if ! echo "$INPUT" | jq empty 2>/dev/null; then echo "Invalid JSON input" >&2 exit 1fi
# Main logic with fallbacks{ # Your hook logic here :} || { echo "Non-critical error, continuing" >&2 exit 0 # Don't block Claude}
Set timeouts to prevent hanging:
{ "type": "command", "command": "your-command", "timeout": 5000 // 5 seconds}
Run async when possible:
# Don't block Claude for non-critical tasks{ sleep 2 curl -X POST https://webhook.site/...} &
Cache expensive operations:
CACHE_FILE="/tmp/claude-hook-cache-$(date +%Y%m%d)"if [[ ! -f "$CACHE_FILE" ]]; then expensive_operation > "$CACHE_FILE"ficat "$CACHE_FILE"
# Secure hook practices
# 1. Validate and sanitize inputFILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path' | sed 's/[^a-zA-Z0-9./_-]//g')
# 2. Use absolute pathsSAFE_DIR="/home/user/project"if [[ ! "$FILE_PATH" =~ ^"$SAFE_DIR" ]]; then echo "Access denied: outside project directory" >&2 exit 2fi
# 3. Quote all variablescommand "$FILE_PATH" # Badcommand "$FILE_PATH" # Good
# 4. Avoid eval and injectioneval "$USER_INPUT" # Never do this
# Launch with debug outputclaude --debug
# Check hook execution[DEBUG] Executing hooks for PostToolUse:Edit[DEBUG] Hook command completed with status 0
# Test your hook with sample inputecho '{ "hook_event_name": "PreToolUse", "tool_name": "Edit", "tool_input": { "file_path": "test.js" }}' | ./my-hook.sh
Issue | Solution |
---|---|
Hook not triggering | Check matcher pattern and JSON syntax |
Permission denied | Make script executable: chmod +x hook.sh |
Command not found | Use absolute paths or check PATH |
Timeout errors | Increase timeout or optimize script |
{ "hooks": { "PreToolUse": [ { "matcher": "Edit", "hooks": [ { "type": "command", "command": ".claude/hooks/pre-edit-checks.sh" } ] } ], "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": ".claude/hooks/format-and-lint.sh" }, { "type": "command", "command": ".claude/hooks/run-affected-tests.sh" } ] } ], "Stop": [ { "hooks": [ { "type": "command", "command": ".claude/hooks/generate-commit-message.sh" } ] } ] }}
Share hooks with your team:
# In your repogit add .claude/hooks/ .claude/settings.jsongit commit -m "Add Claude Code automation hooks"git push
# Team members get automation automatically!
Master these advanced hook patterns: