Skip to main content

15 - Hooks: Event-Driven Automation in Claude Code

Automate your workflow with Claude Code\

What do you think about this article?

At a Glance

Automate your workflow with Claude Code\

Hooks: Event-Driven Automation in Claude Code

Claude Code is a powerful tool on its own, but its true strength emerges when you extend it with an event-driven automation system: Hooks. Hooks are shell commands that are automatically executed in response to specific events during the lifecycle of a Claude Code session, transforming a reactive AI assistant into a proactive and customized development system.

Unlike MCP servers, which require creating separate processes and communication through structured protocols, hooks operate as simple shell scripts executed directly by the Claude Code process. This makes them extremely lightweight, quick to implement, and perfect for automations that do not require the complexity of a dedicated server. In this article we will explore every aspect of hooks in depth: from configuration to creating complex automations, from real-world use cases to best practices for effective usage.

What You Will Learn

  • Understand the event-driven hook system of Claude Code
  • Configure hooks in JSON settings files
  • Master all 6 available hook events
  • Create custom hooks for complex automations
  • Use community hooks like Britfix, CC Notify, and cchooks
  • Manage errors, performance, and security in hooks
  • Implement advanced inter-agent communication patterns
  • Apply best practices for a robust hooks ecosystem

Series Overview

#ArticleFocus
1Claude as Technical PartnerSetup and first steps
2Context and Project SetupCLAUDE.md and configuration
3Ideation and RequirementsMVP and user personas
4Backend and Frontend ArchitectureSpring Boot and Angular
5Code StructureOrganization and conventions
6Advanced Prompt EngineeringAdvanced techniques
7Testing and QualityStrategies and test generation
8Professional DocumentationREADME, API, ADR
9Deploy and DevOpsDocker, CI/CD, monitoring
10Evolution and MaintenanceRefactoring and scalability
11Real Project IntegrationClaude Code in production
12Claude Code CLICommand line mastery
13Agent SkillsExtending Claude Code
14Specialized SubagentsOrchestrating specialized agents
15Hooks and Automation (you are here)Event-driven automation
16Ralph Wiggum MethodAutonomous iterative development
17BMAD MethodAI-driven agile methodology
18Multi-Agent OrchestrationCoordinating agent teams
19Claude CoworkWorking together with Claude
20Security and PrivacyProtecting the workflow
21Monitoring and ProductivityMetrics and performance

1. What Are Claude Code Hooks

Claude Code hooks are user-defined shell commands that are automatically executed in correspondence with specific events during a session. They work similarly to git hooks or development framework lifecycle hooks: they attach to key moments in the workflow and allow injecting custom logic without modifying the agent's core behavior.

The philosophy behind hooks is simple but powerful: instead of asking Claude to execute actions through prompts (which the agent might interpret differently than expected), hooks guarantee deterministic and reliable execution. The command is executed exactly as defined, every time, without variations or creative interpretations.

Hooks vs MCP Servers: When to Use What

FeatureHooksMCP Servers
ComplexityLow - shell scriptsHigh - separate process
LatencyMinimal - direct executionHigher - IPC communication
Model controlNo - the LLM does not decideYes - the LLM chooses when to use them
DeterminismTotal - always executedPartial - depends on context
ConfigurationJSON in settings.jsonDedicated server with SDK
Use casesAutomations, notifications, validationsComplex tools, API integrations
SecurityControlled local executionGranular permissions per tool

Hook System Architecture

The hook system fits into Claude Code's execution cycle as a transparent interception layer. Each time a supported event occurs, Claude Code checks whether hooks are configured for that event, prepares the context as JSON input via stdin, executes the specified shell command, and processes the output to determine any flow modifications.

Hooks receive structured input in JSON format via stdin and can return JSON output via stdout to influence the agent's behavior. This stream-based approach enables the composition of complex processing pipelines while maintaining a simple and universal interface.

Hook Execution Flow
┌──────────────────────┐
│    Trigger Event     │
│  (e.g. PreToolUse)   │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│   Input Preparation  │
│  (JSON via stdin)    │
│  - tool_name         │
│  - tool_input        │
│  - session_id        │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│  Command Execution   │  ← User-defined shell script
│  (shell command)     │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│   Output Processing  │
│  (JSON via stdout)   │
│  - decision: allow   │
│  - decision: block   │
│  - decision: modify  │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│  Flow Continuation   │
│  Claude Code         │
└──────────────────────┘

2. Hook Configuration

Hooks are configured in the .claude/settings.json file within the project directory or in the user's global configuration (~/.claude/settings.json). The configuration structure is a JSON object that maps events to lists of hooks, where each hook specifies the command to execute and optionally a matcher to filter which tools or conditions trigger the hook.

.claude/settings.json - Base Structure
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "command": "/path/to/script.sh"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Bash",
        "command": "python3 /path/to/logger.py"
      }
    ],
    "Stop": [
      {
        "command": "notify-send 'Claude Code' 'Task completed!'"
      }
    ]
  }
}

Hook Properties

Configuration Fields

FieldTypeRequiredDescription
matcherstring (regex)NoRegex pattern to filter activation. Supports pipe for logical OR.
commandstringYesShell command to execute. Can be an absolute or relative path.
timeoutnumberNoTimeout in milliseconds. Default: 60000 (60 seconds).

The matcher field is particularly powerful because it supports complete regular expressions. This means you can create hooks that respond only to specific tools or tool combinations, keeping the system reactive and performant.

Advanced Matcher Examples
// Single tool
"matcher": "Write"

// Multiple tools with OR
"matcher": "Write|Edit|MultiEdit"

// Complex regex pattern
"matcher": "^(Bash|Terminal)$"

// All tools (no matcher = wildcard)
// Omit the matcher field

// Tools starting with "File"
"matcher": "^File.*"

// Exclude specific tools (regex negation)
"matcher": "^(?!Read).*$"

Configuration Levels

Claude Code supports three hierarchical configuration levels for hooks, allowing you to define automations at different levels of granularity:

  • Project (.claude/settings.json): Hooks specific to the current repository. Ideal for project-related automations like linting, formatting, or team notifications.
  • User (~/.claude/settings.json): Global hooks that apply to all user sessions. Perfect for personal preferences like desktop notifications or logging.
  • Enterprise (/etc/claude/settings.json): Organization-level hooks. Used for corporate policies like audit logging or compliance checks.

When hooks exist at multiple levels for the same event, they are all executed in order of precedence: enterprise first, then user, finally project. This ensures that corporate policies are always respected, regardless of local configurations.

3. The 6 Hook Events in Detail

Claude Code exposes six distinct events to which hooks can be attached. Each event corresponds to a precise moment in the session lifecycle and provides a specific context through JSON input. Understanding each event in depth is fundamental for designing effective and robust automations.

3.1 PreToolUse: Intercept Before Execution

The PreToolUse event fires before Claude Code executes any tool. It is the most powerful interception point because it allows you to inspect, modify, or block the execution of a tool before it produces effects in the system.

JSON Input for PreToolUse

FieldTypeDescription
tool_namestringTool name (e.g. "Write", "Bash", "Edit")
tool_inputobjectParameters passed to the tool
session_idstringUnique session identifier

The hook can return a JSON output with three possible decisions:

  • "decision": "allow" - Allows the tool to execute without modifications
  • "decision": "block" - Completely blocks execution. Requires a reason field with the justification.
  • "decision": "modify" - Modifies the tool's parameters before execution. Requires a tool_input field with updated parameters.
pre-tool-guard.sh - Block dangerous operations
#!/bin/bash
# PreToolUse hook: block operations on protected files

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input')

# Block writing to critical configuration files
if [ "$TOOL_NAME" = "Write" ] || [ "$TOOL_NAME" = "Edit" ]; then
  FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // .path // ""')

  # Protected files list
  PROTECTED_FILES=(".env" ".env.production" "secrets.json" "credentials.yaml")

  for PROTECTED in "${PROTECTED_FILES[@]}"; do
    if [[ "$FILE_PATH" == *"$PROTECTED"* ]]; then
      echo "{\"decision\": \"block\", \"reason\": \"Protected file: $PROTECTED. Manual modification required.\"}"
      exit 0
    fi
  done
fi

# Block destructive Bash commands
if [ "$TOOL_NAME" = "Bash" ]; then
  COMMAND=$(echo "$TOOL_INPUT" | jq -r '.command // ""')

  # Dangerous patterns
  if echo "$COMMAND" | grep -qE "rm\s+-rf\s+/|DROP\s+TABLE|DELETE\s+FROM.*WHERE\s+1=1|format\s+c:"; then
    echo "{\"decision\": \"block\", \"reason\": \"Potentially destructive command blocked.\"}"
    exit 0
  fi
fi

# Allow everything else
echo '{"decision": "allow"}'
auto-format.sh - Modify parameters before execution
#!/bin/bash
# PreToolUse hook: automatically adds headers to new files

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')

if [ "$TOOL_NAME" = "Write" ]; then
  FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
  CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content')

  # Add copyright header to TypeScript files
  if [[ "$FILE_PATH" == *.ts ]]; then
    HEADER="// Copyright $(date +%Y) - MyCompany\n// Auto-generated header by Claude Code hook\n\n"
    MODIFIED_CONTENT="${HEADER}${CONTENT}"

    echo "$INPUT" | jq --arg content "$MODIFIED_CONTENT" \
      '{decision: "modify", tool_input: (.tool_input + {content: $content})}'
    exit 0
  fi
fi

echo '{"decision": "allow"}'

3.2 PostToolUse: React After Execution

The PostToolUse event fires after a tool has completed its execution. It is the ideal point for logging, result validation, output transformation, and consequent actions based on the operation's result.

Unlike PreToolUse, PostToolUse cannot block or modify the operation (which has already occurred), but it can influence how Claude interprets the result and can execute side actions such as updating logs, sending notifications, or triggering external pipelines.

JSON Input for PostToolUse

FieldTypeDescription
tool_namestringName of the tool executed
tool_inputobjectParameters used for execution
tool_outputstringTool result
session_idstringSession identifier
post-tool-logger.py - Structured operation logging
#!/usr/bin/env python3
"""PostToolUse hook: logs all file write operations."""

import json
import sys
from datetime import datetime
from pathlib import Path

def main():
    input_data = json.loads(sys.stdin.read())

    tool_name = input_data.get("tool_name", "")
    tool_input = input_data.get("tool_input", {})
    session_id = input_data.get("session_id", "unknown")

    # Log only file modification operations
    if tool_name in ("Write", "Edit", "MultiEdit"):
        log_entry = {
            "timestamp": datetime.now().isoformat(),
            "session_id": session_id,
            "tool": tool_name,
            "file": tool_input.get("file_path", "N/A"),
            "operation": "write" if tool_name == "Write" else "edit"
        }

        log_file = Path(".claude/hooks/activity.log")
        log_file.parent.mkdir(parents=True, exist_ok=True)

        with open(log_file, "a") as f:
            f.write(json.dumps(log_entry) + "\n")

    # Log executed Bash commands
    if tool_name == "Bash":
        command = tool_input.get("command", "")
        log_entry = {
            "timestamp": datetime.now().isoformat(),
            "session_id": session_id,
            "tool": "Bash",
            "command": command[:200],  # Truncate for safety
            "exit_code": "success"
        }

        log_file = Path(".claude/hooks/bash-history.log")
        log_file.parent.mkdir(parents=True, exist_ok=True)

        with open(log_file, "a") as f:
            f.write(json.dumps(log_entry) + "\n")

if __name__ == "__main__":
    main()

3.3 UserPromptSubmit: Intercept User Prompts

The UserPromptSubmit event fires when the user sends a prompt to Claude Code. This hook allows you to pre-process, enrich, or filter user messages before they reach the model. It is particularly useful for adding automatic context, applying prompt templates, or implementing security filters on incoming messages.

enrich-prompt.sh - Enrich prompts with context
#!/bin/bash
# UserPromptSubmit hook: adds automatic Git context

INPUT=$(cat)
PROMPT=$(echo "$INPUT" | jq -r '.prompt')

# Add current branch information
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "N/A")
LAST_COMMIT=$(git log --oneline -1 2>/dev/null || echo "N/A")
DIRTY_FILES=$(git status --porcelain 2>/dev/null | wc -l)

# Create enriched context
CONTEXT="[Git Context: branch=$BRANCH, last_commit=$LAST_COMMIT, dirty_files=$DIRTY_FILES]"

# Modify the prompt by adding context
ENRICHED_PROMPT="$CONTEXT\n\n$PROMPT"

echo "$INPUT" | jq --arg prompt "$ENRICHED_PROMPT" \
  '{decision: "modify", prompt: $prompt}'

3.4 Stop: Actions at the End of Generation

The Stop event fires when Claude Code finishes generating a response or completes a task. It is the ideal point for notifications, cleanup, report generation, and any action that should happen at the end of an interaction. This hook is particularly valuable for long tasks where the user might not be in front of the terminal.

notify-complete.sh - Desktop notification on completion
#!/bin/bash
# Stop hook: sends desktop notification when Claude completes

INPUT=$(cat)
STOP_REASON=$(echo "$INPUT" | jq -r '.stop_reason // "completed"')

# Notification on Linux (notify-send)
if command -v notify-send &> /dev/null; then
  notify-send "Claude Code" "Task completed: $STOP_REASON" \
    --icon=dialog-information \
    --urgency=normal
fi

# Notification on macOS (osascript)
if command -v osascript &> /dev/null; then
  osascript -e "display notification \"Task completed: $STOP_REASON\" with title \"Claude Code\""
fi

# Completion sound
if command -v aplay &> /dev/null; then
  aplay /usr/share/sounds/freedesktop/stereo/complete.oga 2>/dev/null &
elif command -v afplay &> /dev/null; then
  afplay /System/Library/Sounds/Glass.aiff &
fi

3.5 SessionStart: Session Initialization

The SessionStart event fires at the beginning of every new Claude Code session. It is the ideal moment for environment setup, loading additional context, validating prerequisites, and preparing the resources needed for the work session.

session-init.sh - Environment setup at startup
#!/bin/bash
# SessionStart hook: prepares the development environment

# Verify prerequisites
echo "Verifying development environment..." >&2

# Check that Node.js is available
if ! command -v node &> /dev/null; then
  echo '{"warning": "Node.js not found. Some features may not work."}' >&2
fi

# Check that the development database is reachable
if command -v pg_isready &> /dev/null; then
  if ! pg_isready -q 2>/dev/null; then
    echo '{"warning": "PostgreSQL not reachable. Start the database."}' >&2
  fi
fi

# Load environment variables from .env
if [ -f ".env" ]; then
  export $(grep -v '^#' .env | xargs) 2>/dev/null
fi

# Record session start
echo "{\"session_start\": \"$(date -Iseconds)\", \"cwd\": \"$(pwd)\"}" \
  >> .claude/hooks/sessions.log 2>/dev/null

# Clean up temporary files from previous sessions
find /tmp -name "claude-hook-*" -mtime +1 -delete 2>/dev/null

3.6 SessionEnd: Cleanup and Final Report

The SessionEnd event fires when a Claude Code session is closed. It is the perfect point for saving state, generating session reports, performing cleanup, and archiving useful information for future sessions.

session-report.py - Automatic end-of-session report
#!/usr/bin/env python3
"""SessionEnd hook: generates a work session report."""

import json
import sys
from datetime import datetime
from pathlib import Path

def main():
    input_data = json.loads(sys.stdin.read())
    session_id = input_data.get("session_id", "unknown")

    # Read the activity log
    log_file = Path(".claude/hooks/activity.log")
    activities = []
    if log_file.exists():
        with open(log_file) as f:
            for line in f:
                try:
                    entry = json.loads(line.strip())
                    if entry.get("session_id") == session_id:
                        activities.append(entry)
                except json.JSONDecodeError:
                    continue

    # Generate report
    report = {
        "session_id": session_id,
        "end_time": datetime.now().isoformat(),
        "total_operations": len(activities),
        "files_modified": list(set(
            a.get("file", "") for a in activities
            if a.get("tool") in ("Write", "Edit", "MultiEdit")
        )),
        "bash_commands": sum(
            1 for a in activities if a.get("tool") == "Bash"
        )
    }

    # Save report
    reports_dir = Path(".claude/hooks/reports")
    reports_dir.mkdir(parents=True, exist_ok=True)
    report_file = reports_dir / f"session-{session_id[:8]}.json"

    with open(report_file, "w") as f:
        json.dump(report, f, indent=2)

    print(f"Session report saved: {report_file}", file=sys.stderr)

if __name__ == "__main__":
    main()

Hook Events Summary

EventWhen It FiresCan Block?Can Modify?
PreToolUseBefore a tool executesYesYes (tool parameters)
PostToolUseAfter a tool executesNoNo (side effects only)
UserPromptSubmitWhen the user sends a promptYesYes (prompt text)
StopWhen Claude finishes generatingNoNo
SessionStartAt the beginning of a new sessionNoNo
SessionEndWhen the session closesNoNo

4. Community Hooks: Ready-to-Use Solutions

The Claude Code community has developed a series of sophisticated hooks that solve common developer needs. These open-source projects demonstrate the versatility of the hook system and can be used directly or as a base for customizations.

4.1 Britfix: Automatic American-to-British English Conversion

Britfix is a PostToolUse hook that automatically converts American English spelling to British English spelling in files written by Claude Code. It was created for teams that work with British standards but use an AI model trained predominantly on American texts.

The hook intercepts every file write operation, analyzes the content for words with American spelling (such as "color", "optimize", "center") and replaces them with the British variants ("colour", "optimise", "centre"). The process is transparent and requires no manual intervention.

Britfix Configuration
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "command": "npx britfix --auto-fix"
      }
    ]
  }
}

4.2 CC Notify: Intelligent Desktop Notifications

CC Notify is an advanced desktop notification system that goes beyond simple completion notifications. It supports differentiated notifications by event type, notification history, integration with messaging systems (Slack, Discord, Telegram), and granular configuration by activity type.

A particularly useful feature of CC Notify is its ability to estimate the remaining time for long tasks based on the history of previous sessions, allowing the user to plan their time more effectively while Claude Code works in the background.

CC Notify Configuration
{
  "hooks": {
    "Stop": [
      {
        "command": "cc-notify --channel slack --sound chime --message 'Task completed'"
      }
    ],
    "SessionStart": [
      {
        "command": "cc-notify --channel desktop --message 'Claude session started'"
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "cc-notify --log-activity bash"
      }
    ]
  }
}

4.3 cchooks: Python SDK for Professional Hooks

cchooks is a Python SDK that drastically simplifies the creation of complex hooks. Instead of writing shell scripts with manual JSON parsing, cchooks provides typed Python decorators, automatic input/output handling, and a utility library for common operations.

Hook with cchooks SDK
#!/usr/bin/env python3
"""Hook created with cchooks SDK."""

from cchooks import hook, PreToolUseInput, HookDecision

@hook("PreToolUse")
def validate_file_size(event: PreToolUseInput) -> HookDecision:
    """Block writing files that are too large."""

    if event.tool_name == "Write":
        content = event.tool_input.get("content", "")
        max_lines = 500

        if content.count("\n") > max_lines:
            return HookDecision.block(
                reason=f"File too large: {content.count(chr(10))} lines "
                       f"(max {max_lines}). Split into smaller files."
            )

    return HookDecision.allow()

@hook("PostToolUse")
def auto_lint(event):
    """Runs automatic linting after every file modification."""
    import subprocess

    if event.tool_name in ("Write", "Edit"):
        file_path = event.tool_input.get("file_path", "")

        if file_path.endswith(".ts") or file_path.endswith(".js"):
            subprocess.run(
                ["npx", "eslint", "--fix", file_path],
                capture_output=True, timeout=30
            )
        elif file_path.endswith(".py"):
            subprocess.run(
                ["python3", "-m", "black", file_path],
                capture_output=True, timeout=30
            )

4.4 Claude Code Hook Comms: Real-Time Inter-Agent Communication

Claude Code Hook Comms enables real-time communication between multiple Claude Code instances working on the same project or related projects. It uses a messaging system based on shared files or sockets to coordinate the activities of multiple agents, avoiding conflicts and work duplication.

This hook is particularly useful in multi-agent development scenarios where different Claude Code instances work on different aspects of a project (for example frontend and backend) and need to synchronize on changes to shared interfaces.

Inter-Agent Communication Example
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "command": "claude-hook-comms publish --topic file-changes --data-from-stdin"
      }
    ],
    "SessionStart": [
      {
        "command": "claude-hook-comms subscribe --topic file-changes --handler sync-handler.sh"
      }
    ]
  }
}

// sync-handler.sh - Handles file change notifications
#!/bin/bash
INPUT=$(cat)
CHANGED_FILE=$(echo "$INPUT" | jq -r '.file_path')
AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id')

echo "[SYNC] Agent $AGENT_ID modified: $CHANGED_FILE" >> .claude/hooks/sync.log

5. Creating Custom Hooks: Step-by-Step Guide

Creating custom hooks requires understanding the input/output format, choosing the appropriate scripting language, and paying attention to robustness and efficiency. This section provides a complete path from creation to production deployment of professional hooks.

Step 1: Define the Objective

Before writing code, it is essential to clearly define what the hook should do:

  • Which event? Identify the exact moment when the hook should intervene
  • Which action? Define what it should do (block, modify, log, notify)
  • Which conditions? Specify when the hook should activate (matcher)
  • What impact? Evaluate the consequences of the hook on the workflow

Step 2: Write the Script

template-hook.sh - Base Hook Template
#!/bin/bash
set -euo pipefail

# Read JSON input from stdin
INPUT=$(cat)

# Extract common fields
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""')

# ================================
# HOOK LOGIC
# ================================

# Example: log the operation
echo "[$(date -Iseconds)] Hook triggered: tool=$TOOL_NAME session=$SESSION_ID" >&2

# ================================
# OUTPUT (only for PreToolUse/UserPromptSubmit)
# ================================

# Option 1: Allow
echo '{"decision": "allow"}'

# Option 2: Block
# echo '{"decision": "block", "reason": "Reason for blocking"}'

# Option 3: Modify
# echo '{"decision": "modify", "tool_input": {...}}'

Step 3: Test the Hook

Testing hooks is fundamental because errors in the script can block or slow down the entire Claude Code session. Here is a structured approach to testing:

test-hook.sh - Hook testing script
#!/bin/bash
# Test a hook by simulating JSON input

HOOK_SCRIPT="./my-hook.sh"

# Simulate PreToolUse input for Write
echo '{
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/tmp/test.ts",
    "content": "console.log(\"hello\");"
  },
  "session_id": "test-session-001"
}' | bash "$HOOK_SCRIPT"

echo "---"

# Simulate PreToolUse input for Bash with dangerous command
echo '{
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf /"
  },
  "session_id": "test-session-001"
}' | bash "$HOOK_SCRIPT"

echo "---"

# Verify exit code
echo "Exit code: $?"

Step 4: Register and Activate

Registration in .claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "command": "bash .claude/hooks/pre-tool-guard.sh"
      },
      {
        "matcher": "Bash",
        "command": "bash .claude/hooks/bash-validator.sh"
      }
    ],
    "PostToolUse": [
      {
        "command": "python3 .claude/hooks/post-tool-logger.py"
      }
    ],
    "Stop": [
      {
        "command": "bash .claude/hooks/notify-complete.sh"
      }
    ],
    "SessionStart": [
      {
        "command": "bash .claude/hooks/session-init.sh"
      }
    ],
    "SessionEnd": [
      {
        "command": "python3 .claude/hooks/session-report.py"
      }
    ]
  }
}

6. Advanced Hook Patterns

Beyond basic usage, hooks can be combined into sophisticated patterns that solve complex automation needs. This section presents some of the most useful patterns that have emerged from community experience.

Pattern: Auto-Commit on Checkpoint

This pattern automatically creates Git commits at regular intervals during a development session, ensuring that work is never lost and providing granular rollback points.

auto-checkpoint.sh - Automatic commits
#!/bin/bash
# PostToolUse hook: creates automatic checkpoints

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')

# Only for file modification operations
if [ "$TOOL_NAME" = "Write" ] || [ "$TOOL_NAME" = "Edit" ]; then
  FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')

  # Check if there are enough changes for a checkpoint
  CHANGES=$(git diff --stat 2>/dev/null | tail -1)
  NUM_FILES=$(git diff --name-only 2>/dev/null | wc -l)

  if [ "$NUM_FILES" -ge 3 ]; then
    git add -A 2>/dev/null
    git commit -m "checkpoint: auto-save after $NUM_FILES files modified" \
      --no-verify 2>/dev/null
    echo "[CHECKPOINT] Automatic commit: $NUM_FILES files" >&2
  fi
fi

Pattern: Quality Gate Pre-Commit

This pattern automatically runs quality checks (linting, type checking, unit tests) before allowing write operations, ensuring that every modification meets the project's standards.

quality-gate.sh - Automatic quality checks
#!/bin/bash
# PreToolUse hook: quality gate for TypeScript files

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')

if [ "$TOOL_NAME" = "Write" ]; then
  FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
  CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content')

  if [[ "$FILE_PATH" == *.ts ]]; then
    # Write the content to a temporary file for analysis
    TEMP_FILE=$(mktemp /tmp/claude-hook-XXXXXX.ts)
    echo "$CONTENT" > "$TEMP_FILE"

    # Run type checking
    ERRORS=$(npx tsc --noEmit --strict "$TEMP_FILE" 2>&1 || true)

    rm -f "$TEMP_FILE"

    if echo "$ERRORS" | grep -q "error TS"; then
      ERROR_COUNT=$(echo "$ERRORS" | grep -c "error TS")
      echo "{\"decision\": \"block\", \"reason\": \"Type check failed: $ERROR_COUNT TypeScript errors found. Fix before writing.\"}"
      exit 0
    fi
  fi
fi

echo '{"decision": "allow"}'

Pattern: Context Enrichment Pipeline

This pattern automatically enriches every user prompt with relevant contextual information, such as repository state, recently modified files, failed tests, and project metrics.

context-pipeline.sh - Context enrichment
#!/bin/bash
# UserPromptSubmit hook: context enrichment pipeline

INPUT=$(cat)
PROMPT=$(echo "$INPUT" | jq -r '.prompt')

# Gather context
CONTEXT_PARTS=()

# 1. Git state
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ -n "$BRANCH" ]; then
  DIRTY=$(git status --porcelain 2>/dev/null | wc -l)
  CONTEXT_PARTS+=("[Branch: $BRANCH, Modified files: $DIRTY]")
fi

# 2. Recent failed tests
if [ -f "test-results.json" ]; then
  FAILED=$(jq '.numFailedTests // 0' test-results.json 2>/dev/null)
  if [ "$FAILED" -gt 0 ]; then
    CONTEXT_PARTS+=("[WARNING: $FAILED tests failed]")
  fi
fi

# 3. Pending TODOs
TODO_COUNT=$(grep -r "TODO\|FIXME\|HACK" src/ --include="*.ts" 2>/dev/null | wc -l)
if [ "$TODO_COUNT" -gt 0 ]; then
  CONTEXT_PARTS+=("[Open TODOs in code: $TODO_COUNT]")
fi

# Build aggregated context
if [ ${#CONTEXT_PARTS[@]} -gt 0 ]; then
  CONTEXT=$(printf "%s " "${CONTEXT_PARTS[@]}")
  ENRICHED="$CONTEXT\n\n$PROMPT"
  echo "$INPUT" | jq --arg p "$ENRICHED" '{decision: "modify", prompt: $p}'
else
  echo '{"decision": "allow"}'
fi

7. Performance, Security, and Best Practices

Hooks are executed synchronously within Claude Code's lifecycle, which means slow or malfunctioning hooks can significantly degrade the user experience. Following performance and security best practices is essential for maintaining a smooth and protected workflow.

Performance Optimization

Golden Rules for Performant Hooks

RuleMotivationImplementation
Aggressive timeouts Avoid session blocking Set timeout to 5-10 seconds for critical hooks
Specific matchers Reduce unnecessary executions Use precise regex instead of wildcards
Async operations Do not block on slow I/O Delegate heavy operations to background processes
Local cache Avoid expensive recalculations Save intermediate results in temporary files
Early exit Minimize execution time Check exit conditions at the beginning of the script
Controlled logging Avoid excessive disk I/O Use log rotation and severity levels

Hook Security

Hooks execute shell commands with the current user's permissions, which represents both an advantage (full system access) and a risk (uncontrolled execution). It is fundamental to apply rigorous security principles:

  • Principle of least privilege: Each hook should have access only to the resources strictly necessary for its operation
  • Input validation: All data received via stdin must be validated and sanitized before use
  • No hardcoded secrets: API keys, passwords, and tokens must never be included directly in hook scripts
  • Audit trail: Maintain a log of all actions performed by hooks for traceability
  • Periodic review: Regularly review configured hooks to identify emerging risks

Warning: Security Risks

Project hooks (defined in .claude/settings.json) are executed automatically when a Claude Code session is opened in the repository. This means a malicious repository could include harmful hooks. Always verify configuration files before opening sessions in untrusted repositories.

Error Handling

A hook that fails silently is worse than a hook that does not exist. Implementing robust error handling is fundamental for the system's maintainability:

robust-hook.sh - Hook with complete error handling
#!/bin/bash
set -euo pipefail

# Trap to handle errors and cleanup
cleanup() {
  local exit_code=$?
  if [ $exit_code -ne 0 ]; then
    echo "[HOOK ERROR] Exit code: $exit_code" >&2
    echo "{\"error\": \"Hook failed with code $exit_code\"}" >&2
  fi
  # Cleanup temporary resources
  rm -f /tmp/claude-hook-$$.tmp 2>/dev/null
}
trap cleanup EXIT

# Timeout for the entire script
TIMEOUT=10
(
  sleep $TIMEOUT
  echo "[HOOK TIMEOUT] Script exceeded $TIMEOUT seconds" >&2
  kill $$ 2>/dev/null
) &
TIMEOUT_PID=$!

# Main logic with error handling
INPUT=$(cat) || {
  echo '{"decision": "allow"}' # Safe fallback
  exit 0
}

TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null) || TOOL_NAME=""

# ... hook logic ...

# Cancel the timeout timer
kill $TIMEOUT_PID 2>/dev/null || true

echo '{"decision": "allow"}'

8. Complete Example: Hook Ecosystem for an Angular Project

To conclude, we present a complete hook ecosystem configured for an Angular project with SSR, covering all six events and demonstrating how hooks collaborate to create a robust and automated development environment.

.claude/settings.json - Complete Configuration
{
  "hooks": {
    "SessionStart": [
      {
        "command": "bash .claude/hooks/check-dependencies.sh",
        "timeout": 15000
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "command": "bash .claude/hooks/validate-angular-conventions.sh",
        "timeout": 5000
      },
      {
        "matcher": "Bash",
        "command": "bash .claude/hooks/block-dangerous-commands.sh",
        "timeout": 3000
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "command": "bash .claude/hooks/auto-lint-format.sh",
        "timeout": 10000
      },
      {
        "command": "python3 .claude/hooks/activity-tracker.py",
        "timeout": 5000
      }
    ],
    "UserPromptSubmit": [
      {
        "command": "bash .claude/hooks/enrich-with-project-context.sh",
        "timeout": 5000
      }
    ],
    "Stop": [
      {
        "command": "bash .claude/hooks/desktop-notification.sh"
      }
    ],
    "SessionEnd": [
      {
        "command": "python3 .claude/hooks/generate-session-report.py",
        "timeout": 10000
      }
    ]
  }
}
validate-angular-conventions.sh
#!/bin/bash
# PreToolUse: validate Angular conventions for new files

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')

# Verify naming convention for Angular components
if [[ "$FILE_PATH" == *.component.ts ]]; then
  # Check that the name follows kebab-case
  FILENAME=$(basename "$FILE_PATH" .component.ts)
  if ! echo "$FILENAME" | grep -qE '^[a-z][a-z0-9]*(-[a-z0-9]+)*$'; then
    echo "{\"decision\": \"block\", \"reason\": \"Naming convention violated: Angular components must use kebab-case. File: $FILENAME\"}"
    exit 0
  fi
fi

# Verify that service files are in the services/ directory
if [[ "$FILE_PATH" == *.service.ts ]] && [[ "$FILE_PATH" != */services/* ]]; then
  echo "{\"decision\": \"block\", \"reason\": \"Angular services must be in the services/ directory\"}"
  exit 0
fi

echo '{"decision": "allow"}'

9. Debug and Troubleshooting Hooks

When a hook does not work as expected, diagnosis can be complicated by the asynchronous nature and implicit execution of the system. Here are the most effective strategies for identifying and resolving problems.

Debug Techniques

  • Logging to stderr: Everything written to stderr is displayed in the Claude Code terminal. Use echo "debug info" >&2 for debug messages.
  • Dedicated log files: Write detailed output to log files in the .claude/hooks/logs/ directory for post-hoc analysis.
  • Isolated testing: Test each hook independently by simulating JSON input through pipes.
  • Dry run mode: Implement a dry-run mode in hooks that logs actions without executing them.
debug-wrapper.sh - Debug wrapper for hooks
#!/bin/bash
# Wrapper that adds debugging to any hook

HOOK_SCRIPT="$1"
LOG_DIR=".claude/hooks/logs"
mkdir -p "$LOG_DIR"

# Capture input
INPUT=$(cat)
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

# Log input
echo "=== $TIMESTAMP ===" >> "$LOG_DIR/debug.log"
echo "Hook: $HOOK_SCRIPT" >> "$LOG_DIR/debug.log"
echo "Input: $INPUT" >> "$LOG_DIR/debug.log"

# Execute hook and capture output
OUTPUT=$(echo "$INPUT" | bash "$HOOK_SCRIPT" 2>> "$LOG_DIR/debug.log")
EXIT_CODE=$?

# Log output
echo "Output: $OUTPUT" >> "$LOG_DIR/debug.log"
echo "Exit: $EXIT_CODE" >> "$LOG_DIR/debug.log"
echo "" >> "$LOG_DIR/debug.log"

# Return output
echo "$OUTPUT"
exit $EXIT_CODE

Common Problems and Solutions

Quick Troubleshooting

ProblemProbable CauseSolution
Hook does not fire Matcher does not match Verify the exact tool name and regex pattern
Hook slows down Claude Timeout too high or slow operation Reduce timeout, delegate to background process
Output not processed Malformed JSON on stdout Validate output with jq and verify encoding
Permission denied Script not executable Run chmod +x script.sh
Missing environment variables Shell does not inherit environment Explicitly load .env in the script
Hook works manually but not from Claude Different PATH in the hook environment Use absolute paths for all commands

Conclusion

Claude Code hooks represent the bridge between a reactive AI assistant and a proactive and customized development system. Through six strategic interception points, it is possible to automate validations, enrich context, protect the project from dangerous operations, and coordinate complex workflows.

The true power of hooks emerges from their composition: individually they are simple scripts, but combined they create an automation ecosystem that radically transforms how you interact with the AI agent. From auto-linting to completion notification, from quality gates to inter-agent communication, hooks make every Claude Code session more efficient, secure, and productive.

Key Takeaways

  • 6 events cover the entire cycle: From SessionStart to SessionEnd, every moment is interceptable
  • Guaranteed determinism: Unlike prompts, hooks always execute exactly what you define
  • Active community: Britfix, CC Notify, cchooks, and Hook Comms solve common needs
  • Powerful composition: Simple hooks combined create sophisticated automations
  • Security first: Validate input, use timeouts, and verify hooks from external repositories
  • Critical performance: Synchronous hooks require attention to timeouts and early exit

In the next article we will explore the Ralph Wiggum Method: how to configure Claude Code to execute completely autonomous development loops, iterating on a project without human intervention until the defined objectives are achieved.

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects