Agent skill

hooks-configuration

Claude Code hooks configuration and development. Covers hook lifecycle events, configuration patterns, input/output schemas, and common automation use cases. Use when user mentions hooks, automation, PreToolUse, PostToolUse, SessionStart, SubagentStart, or needs to enforce consistent behavior in Claude Code workflows.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/hooks-configuration

SKILL.md

Claude Code Hooks Configuration

Expert knowledge for configuring and developing Claude Code hooks to automate workflows and enforce best practices.

Core Concepts

What Are Hooks? Hooks are user-defined shell commands that execute at specific points in Claude Code's lifecycle. Unlike relying on Claude to "decide" to run something, hooks provide deterministic, guaranteed execution.

Why Use Hooks?

  • Enforce code formatting automatically
  • Block dangerous commands before execution
  • Inject context at session start
  • Log commands for audit trails
  • Send notifications when tasks complete

Hook Lifecycle Events

Event When It Fires Key Use Cases
SessionStart Session begins/resumes Environment setup, context loading
UserPromptSubmit User submits prompt Input validation, context injection
PreToolUse Before tool execution Permission control, blocking dangerous ops
PostToolUse After tool completes Auto-formatting, logging, validation
Stop Agent finishes Notifications, git reminders
SubagentStart Subagent is about to start Input modification, context injection
SubagentStop Subagent finishes Task completion evaluation
PreCompact Before context compaction Transcript backup
Notification Claude sends notification Custom alerts
SessionEnd Session terminates Cleanup, state persistence

Configuration

File Locations

Hooks are configured in settings files:

  • ~/.claude/settings.json - User-level (applies everywhere)
  • .claude/settings.json - Project-level (committed to repo)
  • .claude/settings.local.json - Local project (not committed)

Claude Code merges all matching hooks from all files.

Frontmatter Hooks (Skills and Commands)

Hooks can also be defined directly in skill and command frontmatter using the hooks field:

yaml
---
name: my-skill
description: A skill with hooks
allowed-tools: Bash, Read
hooks:
  PreToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: "echo 'Pre-tool hook from skill'"
          timeout: 10
---

This allows skills and commands to define their own hooks that are active only when that skill/command is in use.

Basic Structure

json
{
  "hooks": {
    "EventName": [
      {
        "matcher": "ToolPattern",
        "hooks": [
          {
            "type": "command",
            "command": "your-command-here",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Matcher Patterns

  • Exact match: "Bash" - matches exactly "Bash" tool
  • Regex patterns: "Edit|Write" - matches either tool
  • Wildcards: "Notebook.*" - matches tools starting with "Notebook"
  • All tools: "*" - matches everything
  • MCP tools: "mcp__server__tool" - targets MCP server tools

Input Schema

Hooks receive JSON via stdin with these common fields:

json
{
  "session_id": "unique-session-id",
  "transcript_path": "/path/to/conversation.json",
  "cwd": "/current/working/directory",
  "permission_mode": "mode",
  "hook_event_name": "PreToolUse"
}

PreToolUse additional fields:

json
{
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test"
  }
}

PostToolUse additional fields:

json
{
  "tool_name": "Bash",
  "tool_input": { ... },
  "tool_response": { ... }
}

SubagentStart additional fields:

json
{
  "subagent_type": "Explore",
  "subagent_prompt": "original prompt text",
  "subagent_model": "claude-sonnet-4-20250514"
}

Output Schema

Exit Codes

  • 0: Success (command allowed)
  • 2: Blocking error (stderr shown to Claude, operation blocked)
  • Other: Non-blocking error (logged in verbose mode)

JSON Response (optional)

PreToolUse:

json
{
  "permissionDecision": "allow|deny|ask",
  "permissionDecisionReason": "explanation",
  "updatedInput": { "modified": "input" }
}

Stop/SubagentStop:

json
{
  "decision": "block",
  "reason": "required explanation for continuing"
}

SubagentStart (input modification):

json
{
  "updatedPrompt": "modified prompt text to inject context or modify behavior"
}

SessionStart:

json
{
  "additionalContext": "Information to inject into session"
}

Common Hook Patterns

Block Dangerous Commands (PreToolUse)

bash
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# Block rm -rf /
if echo "$COMMAND" | grep -Eq 'rm\s+(-rf|-fr)\s+/'; then
    echo "BLOCKED: Refusing to run destructive command on root" >&2
    exit 2
fi

exit 0

Auto-Format After Edits (PostToolUse)

bash
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ "$FILE" == *.py ]]; then
    ruff format "$FILE" 2>/dev/null
    ruff check --fix "$FILE" 2>/dev/null
elif [[ "$FILE" == *.ts ]] || [[ "$FILE" == *.tsx ]]; then
    prettier --write "$FILE" 2>/dev/null
fi

exit 0

Remind About Built-in Tools (PreToolUse)

bash
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

if echo "$COMMAND" | grep -Eq '^\s*cat\s+[^|><]'; then
    echo "REMINDER: Use the Read tool instead of 'cat'" >&2
    exit 2
fi

exit 0

Load Context at Session Start (SessionStart)

bash
#!/bin/bash
GIT_STATUS=$(git status --short 2>/dev/null | head -5)
BRANCH=$(git branch --show-current 2>/dev/null)

cat << EOF
{
  "additionalContext": "Current branch: $BRANCH\nPending changes:\n$GIT_STATUS"
}
EOF

Inject Context for Subagents (SubagentStart)

bash
#!/bin/bash
INPUT=$(cat)
SUBAGENT_TYPE=$(echo "$INPUT" | jq -r '.subagent_type // empty')
ORIGINAL_PROMPT=$(echo "$INPUT" | jq -r '.subagent_prompt // empty')

# Add project context to Explore agents
if [ "$SUBAGENT_TYPE" = "Explore" ]; then
    PROJECT_INFO="Project uses TypeScript with Bun. Main source in src/."
    cat << EOF
{
  "updatedPrompt": "$PROJECT_INFO\n\n$ORIGINAL_PROMPT"
}
EOF
fi

exit 0

Desktop Notification on Stop (Stop)

bash
#!/bin/bash
# Linux
notify-send "Claude Code" "Task completed" 2>/dev/null

# macOS
osascript -e 'display notification "Task completed" with title "Claude Code"' 2>/dev/null

exit 0

Audit Logging (PostToolUse)

bash
#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // "N/A"')

echo "$(date -Iseconds) | $TOOL | $COMMAND" >> ~/.claude/audit.log
exit 0

Configuration Examples

Anti-Pattern Detection

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash $CLAUDE_PROJECT_DIR/hooks-plugin/hooks/bash-antipatterns.sh",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

Auto-Format Python Files

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'FILE=$(cat | jq -r \".tool_input.file_path\"); [[ \"$FILE\" == *.py ]] && ruff format \"$FILE\"'"
          }
        ]
      }
    ]
  }
}

Git Reminder on Stop

json
{
  "hooks": {
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'changes=$(git status --porcelain | wc -l); [ $changes -gt 0 ] && echo \"Reminder: $changes uncommitted changes\"'"
          }
        ]
      }
    ]
  }
}

Best Practices

Script Development:

  1. Always read input from stdin with cat
  2. Use jq for JSON parsing
  3. Quote all variables to prevent injection
  4. Exit with code 2 to block, 0 to allow
  5. Write blocking messages to stderr
  6. Keep hooks fast (< 5 seconds)

Configuration:

  1. Use $CLAUDE_PROJECT_DIR for portable paths
  2. Set appropriate timeouts (default: 60s)
  3. Use specific matchers over wildcards
  4. Test hooks manually before enabling

Security:

  1. Validate all inputs
  2. Use absolute paths
  3. Avoid touching .env or .git/ directly
  4. Review hook code before deployment

Debugging

Verify hook registration:

/hooks

Enable debug logging:

bash
claude --debug

Test hooks manually:

bash
echo '{"tool_input": {"command": "cat file.txt"}}' | bash your-hook.sh
echo $?  # Check exit code

Available Hooks in This Plugin

  • bash-antipatterns.sh: Detects when Claude uses shell commands instead of built-in tools (cat, grep, sed, timeout, etc.)

See hooks/README.md for full documentation.

Didn't find tool you were looking for?

Be as detailed as possible for better results