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
2— blocks 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:
- Detects when Claude runs a
Bashtool call containingrm - Logs the command to
~/.claude/logs/deletions.logwith a timestamp - Does NOT block the command (just observes)
Then extend it to block rm -rf / or rm -rf ~ with an exit code 2.