Przejdź do głównej zawartości

Hooks System Mastery

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:

  • Notification - When Claude needs permission or is idle
  • SubagentStop - When subagent tasks complete
  • PreCompact - Before conversation compression
  1. Open hooks configuration

    Terminal window
    /hooks
    # Select: PreToolUse
  2. Add a matcher for Edit tool

    Matcher: Edit
  3. Add formatting hook

    Terminal window
    prettier --write "$CLAUDE_FILE_PATHS" 2>/dev/null || true
  4. 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"
}
]
}
]
}
}

Matchers determine which tools trigger your hooks:

PatternDescriptionExample
EditExact matchOnly Edit tool
Edit|WriteMultiple toolsEdit OR Write
Notebook.*Regex patternNotebookEdit, NotebookRead
"" or omittedAll toolsEvery tool call
mcp__.*__.*MCP toolsAll 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 CodeEffectUse Case
0Success, continueNormal flow
2Block with feedbackValidation failed
OtherNon-blocking errorWarning only
security-check.sh
#!/bin/bash
if [[ "$1" =~ rm\ -rf ]]; then
echo "Dangerous command blocked" >&2
exit 2
fi
exit 0
{
"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'"
}
]
}
]
}
}
pre-edit-security.sh
#!/bin/bash
set -e
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
# Prevent editing sensitive files
FORBIDDEN_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
fi
done
# Check for secrets in content
CONTENT=$(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 2
fi
exit 0
  • Folder.claude/
    • Folderhooks/
      • post-edit-typescript.sh
      • pre-bash-validate.sh
      • notification-handler.sh
      • stop-cleanup.sh
post-edit-typescript.sh
#!/bin/bash
# Comprehensive TypeScript workflow
set -e
INPUT=$(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
fi
done
notification-handler.sh
#!/bin/bash
# Platform-aware notifications
INPUT=$(cat)
MESSAGE=$(echo "$INPUT" | jq -r '.message // "Claude needs attention"')
# macOS
if command -v osascript &> /dev/null; then
osascript -e "display notification \"$MESSAGE\" with title \"Claude Code\" sound name \"Glass\""
# Linux with notify-send
elif command -v notify-send &> /dev/null; then
notify-send "Claude Code" "$MESSAGE" --urgency=normal --icon=dialog-information
# Windows WSL
elif 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 file
echo "[$(date)] $MESSAGE" >> ~/.claude/notifications.log
enhance-prompt.sh
#!/bin/bash
# Add context to user prompts
INPUT=$(cat)
PROMPT=$(echo "$INPUT" | jq -r '.prompt')
# Add git context
GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "no-git")
GIT_STATUS=$(git status --porcelain | wc -l)
# Add system context
NODE_VERSION=$(node -v 2>/dev/null || echo "no-node")
# Output enhanced prompt
cat <<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 pipefail
trap 'echo "Hook failed: $?" >&2' ERR
# Safe JSON parsing
INPUT=$(cat)
if ! echo "$INPUT" | jq empty 2>/dev/null; then
echo "Invalid JSON input" >&2
exit 1
fi
# 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:

    Terminal window
    # Don't block Claude for non-critical tasks
    {
    sleep 2
    curl -X POST https://webhook.site/...
    } &
  • Cache expensive operations:

    Terminal window
    CACHE_FILE="/tmp/claude-hook-cache-$(date +%Y%m%d)"
    if [[ ! -f "$CACHE_FILE" ]]; then
    expensive_operation > "$CACHE_FILE"
    fi
    cat "$CACHE_FILE"
Terminal window
# Secure hook practices
# 1. Validate and sanitize input
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path' | sed 's/[^a-zA-Z0-9./_-]//g')
# 2. Use absolute paths
SAFE_DIR="/home/user/project"
if [[ ! "$FILE_PATH" =~ ^"$SAFE_DIR" ]]; then
echo "Access denied: outside project directory" >&2
exit 2
fi
# 3. Quote all variables
command "$FILE_PATH" # Bad
command "$FILE_PATH" # Good
# 4. Avoid eval and injection
eval "$USER_INPUT" # Never do this
Terminal window
# Launch with debug output
claude --debug
# Check hook execution
[DEBUG] Executing hooks for PostToolUse:Edit
[DEBUG] Hook command completed with status 0
Terminal window
# Test your hook with sample input
echo '{
"hook_event_name": "PreToolUse",
"tool_name": "Edit",
"tool_input": {
"file_path": "test.js"
}
}' | ./my-hook.sh
IssueSolution
Hook not triggeringCheck matcher pattern and JSON syntax
Permission deniedMake script executable: chmod +x hook.sh
Command not foundUse absolute paths or check PATH
Timeout errorsIncrease 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:

Terminal window
# In your repo
git add .claude/hooks/ .claude/settings.json
git commit -m "Add Claude Code automation hooks"
git push
# Team members get automation automatically!

Master these advanced hook patterns:

  1. Combine with Custom Commands for powerful macros
  2. Integrate with Memory Management for context-aware hooks
  3. Configure Enterprise Setup for organization-wide automation