🇫🇷 Français

Hooks: Automating Event Reactions

Day 12 - Trigger automatic actions in Claude Code

By Angelo Lima

Hooks allow you to execute automatic actions in response to Claude Code events. They’re the bridge between Claude and your development tools. Today, we’ll see how to use and create them.

What is a Hook?

A hook is an event handler that executes when Claude Code does something specific:

  • Before/after tool execution
  • At session start/end
  • When user submits a prompt
  • etc.

The 9 Hook Types

Hook Trigger Can Block
SessionStart Session start No
SessionEnd Session end No
PreToolUse Before tool execution Yes
PostToolUse After tool execution No
UserPromptSubmit Prompt submission Yes
Notification Claude notification No
Stop User stop No
SubagentStop Subagent end No
PreCompact Before context compaction No

Hook Configuration

Location

In .claude/settings.json:

{
  "hooks": {
    "HookName": [
      {
        "matcher": "optional-pattern",
        "hooks": [
          {
            "type": "command",
            "command": "path/to/script.sh"
          }
        ]
      }
    ]
  }
}

Hook Structure

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "prettier --write $FILE"
          }
        ]
      }
    ]
  }
}

Explanation:

  • PostToolUse: Triggers after tool use
  • matcher: "Edit": Only when “Edit” tool is used
  • command: The command to execute

Useful Hook Examples

Hook: Auto-format After Edit

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "prettier --write $EDITED_FILE"
          }
        ]
      },
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "prettier --write $WRITTEN_FILE"
          }
        ]
      }
    ]
  }
}

Hook: Git Check Before Exit

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/git-check.sh"
          }
        ]
      }
    ]
  }
}

git-check.sh script:

#!/bin/bash

# Check for uncommitted changes
if [[ -n $(git status --porcelain) ]]; then
    echo "⚠️  Warning: uncommitted changes!"
    git status --short
fi

Hook: Block Dangerous Patterns

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/security-check.sh"
          }
        ]
      }
    ]
  }
}

security-check.sh script:

#!/bin/bash

# Read command from stdin
read -r command

# Dangerous patterns
dangerous_patterns=(
    "rm -rf /"
    "rm -rf ~"
    "sudo rm"
    "> /dev/"
    "mkfs"
    "dd if="
    "chmod 777"
)

for pattern in "${dangerous_patterns[@]}"; do
    if [[ "$command" == *"$pattern"* ]]; then
        echo "BLOCKED: Dangerous command detected: $pattern"
        exit 1  # Exit 1 = block action
    fi
done

exit 0  # Exit 0 = allow

Hook: Log Actions

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/log-action.sh"
          }
        ]
      }
    ]
  }
}

log-action.sh script:

#!/bin/bash

# Read tool info from stdin (JSON)
read -r json

# Extract info with jq
tool=$(echo "$json" | jq -r '.tool')
timestamp=$(date +"%Y-%m-%d %H:%M:%S")

# Log
echo "[$timestamp] Tool: $tool" >> ~/.claude/logs/actions.log

Hook: Task Completion Notification

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/notify.sh"
          }
        ]
      }
    ]
  }
}

notify.sh script (macOS):

#!/bin/bash
osascript -e 'display notification "Claude has finished" with title "Claude Code"'

notify.sh script (Linux):

#!/bin/bash
notify-send "Claude Code" "Claude has finished"

Hook: Environment Setup at Start

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/session-start.sh"
          }
        ]
      }
    ]
  }
}

session-start.sh script:

#!/bin/bash

# Activate Python virtual environment if present
if [[ -f ".venv/bin/activate" ]]; then
    source .venv/bin/activate
fi

# Load environment variables
if [[ -f ".env.development" ]]; then
    export $(grep -v '^#' .env.development | xargs)
fi

# Check prerequisites
command -v node >/dev/null || echo "⚠️  Node.js not found"
command -v npm >/dev/null || echo "⚠️  npm not found"

Matcher Patterns

No Matcher (All Events)

{
  "matcher": "",
  "hooks": [...]
}

Match Specific Tool

{
  "matcher": "Bash",
  "hooks": [...]
}

Match with Regex

{
  "matcher": "Bash\\(npm.*\\)",
  "hooks": [...]
}

Available Data

Hooks receive data via stdin in JSON format:

PreToolUse / PostToolUse

{
  "tool": "Edit",
  "input": {
    "file_path": "/path/to/file.ts",
    "old_string": "...",
    "new_string": "..."
  },
  "output": "..." // Only for PostToolUse
}

SessionStart

{
  "cwd": "/path/to/project",
  "model": "claude-sonnet-4-5-20250929",
  "sessionId": "abc123"
}

Blocking Actions

A PreToolUse or UserPromptSubmit hook can block the action:

# Exit code 0 = allow
exit 0

# Exit code != 0 = block
exit 1

Block message:

echo "BLOCKED: Reason for blocking"
exit 1

Hook Security

⚠️ Warning

Hooks execute with your user permissions. A malicious hook could:

  • Read your files
  • Exfiltrate data
  • Modify your system

Best Practices

  1. Check the code before adding an external hook
  2. Test in isolation in a safe environment
  3. Limit permissions of scripts
  4. Regularly audit installed hooks

Debugging Hooks

Enable Logs

CLAUDE_CODE_DEBUG=hooks claude

Test Script Manually

echo '{"tool": "Edit", "input": {...}}' | ./my-hook.sh
echo $?  # Check exit code
~/.claude/
├── hooks/
│   ├── security-check.sh
│   ├── git-check.sh
│   ├── log-action.sh
│   ├── notify.sh
│   └── session-start.sh
├── logs/
│   └── actions.log
└── settings.json

What’s Coming Tomorrow

In Day 13, we’ll see MCP: Connecting Claude Code to Your Tools - how to integrate GitHub, Jira, databases, and other external services.


This article is part of the “Master Claude Code in 20 Days” series. Day 11: Plugins and Marketplace

Share: