Hooks — Automating Claude Code Workflows
Hooks fire before and after every tool call. Use them to auto-lint, auto-test, send notifications, block dangerous commands, and build workflows that run themselves. Here is the complete playbook.
Every tool call Claude makes — every Edit, every Bash, every Write — passes through two event gates: one before it executes and one after. Hooks are your scripts at those gates. You can run any shell command, inspect context, block execution, enforce quality standards, send notifications, or fire off side effects. No extra tool required. The plumbing is already there.
This is how you build a Claude Code environment that enforces its own quality standards without you having to ask.
The Event Model
Four hook events are available:
PreToolUse — fires before a tool executes. If your script exits with code 2, the tool call is blocked. If it exits with 0, execution continues. Use this for validation: is Claude about to run a dangerous bash command? Is it about to edit a protected file?
PostToolUse — fires after a tool executes successfully. The tool result is available in the environment. Use this for side effects: run the linter after every file edit, run the test suite after a file that matches *.test.* is written, send a webhook after a deployment completes.
Notification — fires when Claude sends a user-facing notification (asking for permission, reporting completion, etc.). Use this to trigger external alerts — a Discord message, a macOS notification, a Slack ping.
Stop — fires when the session ends. Use this to clean up temp files, commit a session log, send a summary.
Configuration
Hooks live in .claude/settings.json at the project root, or in ~/.claude/settings.json for global hooks.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npm run lint --silent 2>&1 | head -20"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/validate-bash.sh"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/session-summary.sh"
}
]
}
]
}
}
The matcher field is a regex matched against the tool name. Edit|Write matches any Edit or Write call. Bash matches only bash. An empty string "" matches everything.
Practical Hook Patterns
Auto-Lint on Every Edit
The most common hook. Catch lint errors immediately, before they accumulate:
#!/bin/bash
# ~/.claude/hooks/auto-lint.sh
# Runs after every Edit or Write
cd "$CLAUDE_PROJECT_DIR" 2>/dev/null || exit 0
npx eslint --fix --quiet "$CLAUDE_TOOL_INPUT_PATH" 2>&1 | head -10
exit 0 # Never block — just report
Auto-Test Trigger
Run the test suite whenever a test file is written:
#!/bin/bash
# Triggered by PostToolUse matcher: "Edit|Write"
if [[ "$CLAUDE_TOOL_INPUT_PATH" == *".test."* ]]; then
cd "$CLAUDE_PROJECT_DIR"
npm test -- --run "$CLAUDE_TOOL_INPUT_PATH" 2>&1 | tail -20
fi
exit 0
Blocking Dangerous Bash Commands
#!/bin/bash
# PreToolUse matcher: "Bash"
# Block rm -rf on anything outside /tmp
CMD="$CLAUDE_TOOL_INPUT_COMMAND"
if echo "$CMD" | grep -qE 'rm -rf [^/tmp]'; then
echo "BLOCKED: rm -rf outside /tmp requires explicit confirmation" >&2
exit 2 # Exit 2 blocks the tool call
fi
exit 0
Discord Notification on Session Stop
#!/bin/bash
# Stop hook — sends session summary to Discord
WEBHOOK_URL="$DISCORD_WEBHOOK_URL"
MESSAGE="Claude Code session ended. Project: $(basename $PWD)"
curl -s -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"content\": \"$MESSAGE\"}" > /dev/null
exit 0
Hooks are the enforcement layer — the people equivalent for automated systems. They make the rules real.
Environment Variables Available to Hooks
Claude Code injects context into the hook environment:
| Variable | Content |
|---|---|
CLAUDE_TOOL_NAME | The tool being invoked (Edit, Bash, Write, etc.) |
CLAUDE_TOOL_INPUT_PATH | File path for file tools |
CLAUDE_TOOL_INPUT_COMMAND | Command string for Bash |
CLAUDE_PROJECT_DIR | Absolute path to the project root |
CLAUDE_SESSION_ID | Unique session identifier |
Use these to write hooks that respond to the specific context — not every edit, but edits to specific paths; not every bash call, but ones that match dangerous patterns.
Security Considerations
Hooks run as shell commands with your full user permissions. A few rules that are not optional:
Never put secrets in hook scripts that are committed. Your Discord webhook URL, database credentials, API keys — these go in environment variables that the hook reads from your shell, not hardcoded in the script source.
Validate hook script inputs. If CLAUDE_TOOL_INPUT_COMMAND is used in a hook, treat it as untrusted input. Do not eval it or pass it to shell expansion without sanitizing.
Test hooks with exit 0 first. Write the hook to do nothing but print what it would do. Confirm the logic is correct before connecting it to real side effects.
Writing Hook Scripts
A few production-grade conventions:
#!/opt/homebrew/bin/bash
# Always use the explicit Homebrew bash path on macOS for launchd compatibility
set -euo pipefail # Fail fast on any error
# Log to a file for debugging — never to stdout unless you want it in Claude's context
LOG="$HOME/.claude/hooks/hook-$(date +%Y%m%d).log"
echo "[$(date -u +%H:%M:%S)] Tool: $CLAUDE_TOOL_NAME Path: ${CLAUDE_TOOL_INPUT_PATH:-none}" >> "$LOG"
# Do your work here
# ...
exit 0
Store hook scripts in ~/.claude/hooks/ and make them executable with chmod +x. Reference them by absolute path in your settings configuration.
Lesson 58 Drill
Implement the auto-lint hook. Create .claude/settings.json with the PostToolUse config pointing to a script that runs your project's linter after every Edit or Write. Run a Claude session, make a change, and observe the hook output appearing in Claude's context. Then add one more hook — a Stop hook that writes a one-line session log entry to ~/.claude/session-log.txt.
Two hooks running and you will feel the difference within a day of use.
Bottom Line
Hooks turn Claude Code from a smart assistant into an enforcing system. PreToolUse validates and blocks. PostToolUse triggers quality side effects. Notification routes alerts. Stop handles cleanup. Exit code 2 blocks. Exit code 0 continues. Keep hook scripts tight, log to files not stdout, never commit secrets, and test with passive scripts before enabling blocking logic. The environment variables give you enough context to build any enforcement rule you need.