Lesson 6 of 20

Lesson 05: Hooks

What Are Hooks?

Hooks are shell commands that Claude Code runs automatically in response to events. They let you extend and automate Claude's behavior without modifying Claude itself.

Hooks are configured in: ~/.claude/settings.json


Hook Events

Event Fires When
PreToolUse Before Claude calls any tool
PostToolUse After Claude calls any tool
UserPromptSubmit When you submit a message
Stop When Claude finishes responding
SubagentStop When a subagent finishes

Hook Configuration

Hooks are defined in ~/.claude/settings.json:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "powershell.exe -File ~/.claude/hooks/notify-done.ps1"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "powershell.exe -File ~/.claude/hooks/log-commands.ps1"
          }
        ]
      }
    ]
  }
}

Matcher

The matcher field filters which tool calls trigger the hook:

  • "" — matches all events
  • "Bash" — matches only Bash tool calls
  • "Edit" — matches only file edits
  • Supports regex patterns

What Hooks Can Do

Read context from stdin

Hooks receive a JSON payload on stdin with details about the event:

{
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm install express"
  }
}

Your hook script can read this and react accordingly.

Control Claude via exit codes

  • Exit 0 — success, Claude continues normally
  • Exit 1 — hook failed but Claude continues (warning only)
  • Exit 2blocks Claude — Claude will not proceed with the action

Exit code 2 is powerful: you can prevent Claude from running certain commands.

Write feedback to stdout

Your hook's stdout is shown to Claude as feedback. This lets hooks communicate with Claude.


Practical Hook Examples

1. Sound Notifications (what peon-ping does)

Play a sound when Claude finishes a response:

# notify-done.ps1
Add-Type -AssemblyName System.Windows.Forms
[System.Media.SystemSounds]::Exclamation.Play()

2. Block Dangerous Commands

Prevent Claude from running git push --force:

#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('command',''))")

if echo "$COMMAND" | grep -q "git push --force"; then
    echo "Force push blocked by hook. Ask the user to confirm first." >&2
    exit 2
fi
exit 0

3. Auto-format on Save

Run prettier whenever Claude edits a TypeScript file:

#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path',''))")

if [[ "$FILE" == *.ts || "$FILE" == *.tsx ]]; then
    npx prettier --write "$FILE" 2>/dev/null
fi
exit 0

4. Log Everything Claude Does

Keep an audit trail:

#!/bin/bash
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "[$TIMESTAMP] $@" >> ~/.claude/logs/activity.log
cat >> ~/.claude/logs/activity.log  # log the full JSON payload
echo "" >> ~/.claude/logs/activity.log
exit 0

5. Validate Before Commit

Run linter before Claude commits:

#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('command',''))")

if echo "$COMMAND" | grep -q "git commit"; then
    npm run lint 2>&1
    if [ $? -ne 0 ]; then
        echo "Lint failed. Fix errors before committing." >&2
        exit 2
    fi
fi
exit 0

Hook Best Practices

Keep hooks fast. Hooks run synchronously — slow hooks block Claude. If you need to do something expensive, run it in the background and exit immediately.

Handle failures gracefully. Always exit 0 unless you intentionally want to block Claude. Unexpected exit 2s will frustrate you.

Test hooks independently. Run your hook script manually with sample JSON input before attaching it to Claude.

Use absolute paths. Hooks run in an unknown working directory. Always use full paths to scripts and binaries.

Log hook output for debugging. Add echo "Hook ran: $TIMESTAMP" >> /tmp/hook-debug.log during development.


The peon-ping Example

Your peon-ping hook (at ~/.claude/hooks/peon-ping/) is a real-world example of a sophisticated hook system:

  • Plays character voice sounds when Claude finishes responding
  • Has multiple sound packs (GLaDOS, Peon, Kerrigan, etc.)
  • Configurable volume, rotation, categories
  • Uses PowerShell with WinForms for audio playback

Study it as a reference for building your own hooks.


Practical Exercise

Build a simple hook that:

  1. Detects when Claude runs a Bash tool call containing rm
  2. Logs the command to ~/.claude/logs/deletions.log with a timestamp
  3. Does NOT block the command (just observes)

Then extend it to block rm -rf / or rm -rf ~ with an exit code 2.