All Days
Day 27

Founder OS — Advanced Hooks Guide

Day 27 / 30
Day 27 · Advanced Hooks

Hooks are your superpower

Lifecycle interceptors that run outside the LLM loop. Zero token cost. Total control. 8 events. 3 exit codes. Infinite patterns.

8
Lifecycle Events
3
Exit Codes
0
Token Cost
Hooks are shell scripts, HTTP calls, or prompt chains that fire at key points in the Claude Code runtime — before tools run, after they complete, at session start, and more.
Section 01 · Event System

The 8 lifecycle events

Every hook targets one of these events. Know when each fires and you can intercept anything.

EventWhen it firesPrimary useType
PreToolUseBefore any tool executesValidation, blocking, context prepsync
PostToolUseAfter any tool completesContext injection, feedback loopssync
SessionStartSession opensLoad git status, PRs, env infoeither
StopClaude finishes respondingNotifications, cost loggingeither
NotificationClaude needs user inputAlert integrations, pagingasync
InstructionsLoadedCLAUDE.md files loadedValidate config, inject extra contextsync
WorktreeCreateNew worktree createdScaffold project, install depseither
WorktreeRemoveWorktree removedCleanup, archivingasync
💡PreToolUse and PostToolUse are the workhorses. They fire for every tool call — Bash, Edit, Write, Read, and more. Use matchers to target specific tools or files.
Section 02 · Exit Codes

Three outcomes, total control

Your hook's exit code tells Claude exactly what to do next.

exit 0
Allow
Proceed as planned. If the hook outputs JSON to stdout, Claude receives it as context — this powers the feedback loop.
exit 1
Soft Fail
Something went wrong but it's recoverable. Claude can retry or adjust its approach. stderr is shown as a warning.
exit 2
Hard Block
Hard stop. Claude cannot proceed with this tool call. stderr becomes the error message. Use for security and compliance rules.
danger-block.sh — PreToolUse
#!/bin/bash CMD="$CLAUDE_TOOL_INPUT_COMMAND" # Block dangerous commands if echo "$CMD" | grep -qE '(rm -rf /|dd if=|mkfs|format)'; then echo "Dangerous command blocked: $CMD" >&2 exit 2 # Hard block — Claude sees this error fi # Warn about sudo if echo "$CMD" | grep -q 'sudo'; then echo "Warning: sudo detected" >&2 exit 1 # Soft fail — Claude can try another way fi exit 0 # Allow
Section 03 · Context Injection

The feedback loop

exit 0 + JSON on stdout = Claude proceeds AND receives your context. This is how you build self-correcting workflows.

🔄The pattern: Tool runs → PostToolUse hook checks output → hook returns JSON with analysis → Claude reads it and self-corrects. Zero manual intervention.
lint-feedback.sh — PostToolUse
#!/bin/bash # Runs after every Edit/Write FILE="$CLAUDE_TOOL_INPUT_PATH" RESULT=$(npx eslint "$FILE" --format json 2>/dev/null) ERRORS=$(echo "$RESULT" | jq '.[0].errorCount // 0') if [ "$ERRORS" -gt 0 ]; then # Claude receives this JSON as context on the next turn echo '{ "lint_status": "failed", "error_count": '"$ERRORS"', "errors": '"$(echo "$RESULT" | jq '[.[0].messages[] | {line,message}]')"' }' fi exit 0 # Always allow — the JSON context does the work
1
Claude edits a filePostToolUse hook fires immediately after the Edit tool completes.
2
Hook runs ESLintChecks the file for errors and formats them as JSON.
3
Hook exits 0 with JSON stdoutClaude receives the lint errors as context on the next turn.
4
Claude auto-fixesReads the errors and corrects them — without you asking.
Section 04 · Hook Types

Three ways to hook in

Pick the right type for your use case. Each has different capabilities and costs.

🐚
Shell Script
Bash, Python, or Node. Full filesystem and CLI access. Zero token cost. The default for most hooks.
zero token cost
🌐
HTTP Webhook
Call any external service — Slack, Notion, analytics, Jira. Fire and forget or wait for response.
zero token cost
🤖
Prompt Hook
Run a separate LLM call on the hook event data. For nuanced decisions that regex cannot handle.
uses tokens
Section 05 · Async Hooks

Fire and forget

Async hooks run in the background. Claude doesn't wait. Exit codes are ignored. Perfect for logging, notifications, and analytics.

Set timeout: 0 in your hook config to make it async. The hook runs but Claude moves on immediately — no blocking, no delays.
settings.json — async hook config
{ "hooks": { "Stop": [{ "matcher": "any", "hooks": [{ "type": "command", "command": ".claude/hooks/notify-done.sh", "timeout": 0 // 0 = async — Claude doesn't wait }] }] } }
Cost loggingWrite token counts to CSV without blocking the session.
Slack and email notificationsAlert your team when Claude finishes a big task.
Analytics and telemetryTrack tool usage, session length, file changes over time.
Not for validation or blockingExit codes are ignored in async mode — use sync hooks for those.
Section 06 · Matcher Patterns

Target exactly what you need

Matchers filter which tool calls trigger your hook. Combine event + tool + path for precision.

Any Bash command
{"tool_name": "Bash"}
Python files only
{"tool_name": "Edit", "file_path": "*.py"}
Specific directory
{"tool_name": "Write", "file_path": "src/**"}
Config files
{"tool_name": "Write", "file_path": "*.config.*"}
Git commands
{"tool_name": "Bash", "command": "git *"}
Any Read/Write
{"tool_name": ["Read", "Write", "Edit"]}
settings.json — matcher
{ "hooks": { "PreToolUse": [{ "matcher": {"tool_name": "Bash"}, "hooks": [{ "type": "command", "command": ".claude/hooks/my-hook.sh" }] }] } }
Section 07 · Production Patterns

10 patterns you can use today

Copy any of these into your .claude/hooks/ folder. Each is production-ready.

ESLint runs after every file edit. If errors are found, the JSON output tells Claude exactly what to fix — zero manual intervention.

PostToolUse → Edit/Write
#!/bin/bash # auto-lint.sh — PostToolUse hook FILE="$CLAUDE_TOOL_INPUT_PATH" RESULT=$(npx eslint "$FILE" --format json 2>/dev/null) ERRORS=$(echo "$RESULT" | jq '.[0].errorCount // 0') if [ "$ERRORS" -gt 0 ]; then MSGS=$(echo "$RESULT" | jq '[.[0].messages[] | {line,message,ruleId}]') echo '{"lint_errors": '"$MSGS"', "file": "'"$FILE"'"}' exit 0 # Claude receives the errors as context fi

Logs token usage and estimated cost to a daily CSV file. Runs asynchronously so Claude is never blocked.

Stop (async)
#!/bin/bash # cost-tracker.sh — async Stop hook DATE=$(date +%Y-%m-%d) TIME=$(date +%H:%M:%S) TOKENS=\${CLAUDE_USAGE_INPUT_TOKENS:-0} COST=$(echo "scale=4; $TOKENS * 0.000003" | bc) echo "$DATE,$TIME,$TOKENS,$COST" \ >> ~/.claude/costs/daily-$DATE.csv

Blocks any write to a non-active client folder. Set ACTIVE_CLIENT in your environment and Claude cannot touch other clients' files.

PreToolUse → Write/Edit
#!/bin/bash # client-protect.sh — PreToolUse hook FILE="$CLAUDE_TOOL_INPUT_PATH" ACTIVE=\${ACTIVE_CLIENT:-} if [[ -n "$ACTIVE" && "$FILE" == *"/clients/"* ]]; then if [[ "$FILE" != *"/clients/$ACTIVE/"* ]]; then echo "BLOCKED: Cannot write to client path" >&2 exit 2 # Hard block fi fi

Scans staged files for API keys, tokens, and passwords before any git commit. Blocks the commit if secrets are detected.

PreToolUse → Bash (git commit)
#!/bin/bash # secret-scan.sh — PreToolUse hook CMD="$CLAUDE_TOOL_INPUT_COMMAND" if [[ "$CMD" == *"git commit"* ]]; then STAGED=$(git diff --cached --name-only) for FILE in $STAGED; do if grep -qE '(api_key|secret|password|token)\s*=\s*["'"'"'][^"'"'"']+["'"'"']' "$FILE"; then echo "SECRET DETECTED in $FILE — commit blocked" >&2 exit 2 fi done fi

Sends a Slack message whenever Claude modifies a config file. Uses an HTTP webhook — no extra dependencies required.

PostToolUse → Write
#!/bin/bash # slack-config.sh — PostToolUse hook FILE="$CLAUDE_TOOL_INPUT_PATH" CONFIG_PATTERN="(next.config|tailwind|tsconfig|package.json|\.env)" if [[ "$FILE" =~ $CONFIG_PATTERN ]]; then curl -s -X POST "$SLACK_WEBHOOK_URL" \ -H 'Content-Type: application/json' \ -d "{\"text\": \"Config file changed: $FILE\"}" fi

A prompt hook evaluates bash commands semantically. Regex cannot catch everything — the LLM decides if a command is safe.

PreToolUse → Bash
{ "hooks": { "PreToolUse": [{ "matcher": {"tool_name": "Bash"}, "hooks": [{ "type": "prompt", "prompt": "Security reviewer: is this command safe? Reply JSON: {allowed: bool, reason: string}", "on_deny": "block" }] }] } }

Creates a timestamped backup of any file before Claude edits it. Maintains the last 10 versions automatically.

PreToolUse → Edit/Write
#!/bin/bash # auto-backup.sh — PreToolUse hook FILE="$CLAUDE_TOOL_INPUT_PATH" BACKUP_DIR=".claude/backups/$(dirname $FILE)" mkdir -p "$BACKUP_DIR" TIMESTAMP=$(date +%Y%m%d_%H%M%S) cp "$FILE" "$BACKUP_DIR/$(basename $FILE).$TIMESTAMP" 2>/dev/null # Keep only last 10 backups ls -t "$BACKUP_DIR/$(basename $FILE)."* 2>/dev/null | tail -n +11 | xargs rm -f

Extracts the Jira ticket ID from the current branch name and moves the ticket to In Review when Claude pushes code.

PostToolUse → Bash (git push)
#!/bin/bash # jira-update.sh — PostToolUse hook CMD="$CLAUDE_TOOL_INPUT_COMMAND" if [[ "$CMD" == *"git push"* ]]; then BRANCH=$(git rev-parse --abbrev-ref HEAD) TICKET=$(echo "$BRANCH" | grep -oE '[A-Z]+-[0-9]+') if [[ -n "$TICKET" ]]; then curl -s -X POST "$JIRA_BASE/rest/api/3/issue/$TICKET/transitions" \ -H "Authorization: Bearer $JIRA_TOKEN" \ -d '{"transition":{"id":"31"}}' # "In Review" fi fi

Plays a sound and sends a macOS notification when Claude finishes. You can walk away and come back when it is done.

Stop
#!/bin/bash # notify-done.sh — Stop hook # macOS osascript -e 'display notification "Claude finished" with title "Claude Code" sound name "Glass"' afplay /System/Library/Sounds/Glass.aiff 2>/dev/null # Linux fallback # notify-send "Claude Code" "Claude finished" 2>/dev/null # paplay /usr/share/sounds/freedesktop/stereo/complete.oga 2>/dev/null

Loads git status, open PRs, and environment info at session start. Claude begins every session fully oriented.

SessionStart
#!/bin/bash # context-inject.sh — SessionStart hook GIT_STATUS=$(git status --short 2>/dev/null | head -20) OPEN_PRS=$(gh pr list --state open --json number,title 2>/dev/null) BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) NODE_VER=$(node --version 2>/dev/null) echo "{ \"git_branch\": \"$BRANCH\", \"node_version\": \"$NODE_VER\", \"open_prs\": \${OPEN_PRS:-[]} }"
Section 08 · Hook Builder

Build your own hook

Configure your hook and get the settings.json config and shell script instantly.

Configuration

Async (fire and forget)

Generated Config

{ "hooks": { "PreToolUse": [ { "matcher": "any", "hooks": [ { "type": "command", "command": ".claude/hooks/my-hook.sh" } ] } ] } }
Section 09 · Knowledge Check

Test your hooks knowledge

5 questions. Pick the best answer.

0/5
Question 1 of 5
What exit code blocks a tool call?
Question 2 of 5
What makes async hooks different from regular hooks?
Question 3 of 5
What are prompt hooks used for?
Question 4 of 5
If a PostToolUse hook exits 0 with JSON on stdout, what happens?
Question 5 of 5
Which lifecycle event fires when Claude finishes responding?
Day 27 Complete

You know advanced hooks

8 lifecycle events. 3 exit codes. 3 hook types. 10 production patterns. One hook builder. Now go ship something great.

🚀Start with the auto-lint feedback loop or the auto-backup hook — both are drop-in and immediately useful. Put them in .claude/hooks/ and add the config to .claude/settings.json.
Events
8 Lifecycle Events
PreToolUse, PostToolUse, SessionStart, Stop, and four more.
Control
Exit Code System
Allow, soft fail, or hard block any tool call.
Patterns
10 Templates
Production-ready hooks you can copy and use today.
don't miss what's next.

playbooks, templates, and tools that actually save you hours. straight to your inbox. no spam. unsubscribe anytime.