Back to Claude Code
Claude CodeIntermediate10 min read

Hooks Automation — Make Claude Code Enforce Your Workflow

Use Claude Code hooks to automate linting, file protection, notifications, and project guardrails without repeating the same review steps by hand

hooksautomationworkflowsafety

Official References: Hooks · Settings · Overview

Curriculum path

  1. CLAUDE.md Mastery — repo memory and rules
  2. Effective Prompting — task framing and constraints
  3. MCP Power Tools — connect tools and live context
  4. Multi-Agent Workflows — delegation and parallel execution
  5. Hooks Automation — local workflow enforcement ← You are here
  6. GitHub Actions Workflows — move repeatable work into team automation

Official docs used in this guide

  • Hook lifecycle and configuration modelHooks
  • Security considerations for hook scriptsHooks
  • Settings surface and local configurationSettings

Why Hooks Matter

Claude Code gets much more useful once it stops being only a smart editor and starts enforcing your team's workflow automatically.

Hooks let Claude run shell commands around tool usage and lifecycle events. That means you can wire in:

  • formatting after edits
  • lint or test checks before risky operations
  • notifications when long tasks finish
  • guards that block edits in sensitive paths
  • context preservation before compaction

In practice, hooks turn repeated review habits into automation.

The Hook Lifecycle

Every time Claude Code runs a tool, this flow happens:

User request
    ↓
PreToolUse hook fires  ← block or pre-check here
    ↓
Tool executes (Edit, Write, Bash, etc.)
    ↓
PostToolUse hook fires ← post-process, format, notify here
    ↓
Claude responds

If a PreToolUse hook exits with a non-zero code, the tool is blocked entirely. That is the mechanism behind file protection guardrails.

Configuring Hooks

Hooks are registered in .claude/settings.json at the project root. Create the file if it does not already exist.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "command": ".claude/hooks/protect-files.sh"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": ".claude/hooks/auto-format.sh"
      }
    ],
    "PreCompact": [
      {
        "matcher": ".*",
        "command": ".claude/hooks/save-context.sh"
      }
    ],
    "Notification": [
      {
        "matcher": ".*",
        "command": ".claude/hooks/notify-done.sh"
      }
    ]
  }
}
  • matcher: a regex that filters which tools trigger the hook. "Edit|Write" matches both the Edit and Write tools.
  • command: the shell command or script path to execute.
  • Hook scripts need execute permission: chmod +x .claude/hooks/your-script.sh.

Practical Hook Examples

1. Auto-format after edits (PostToolUse)

Run Prettier automatically every time a file is modified. This example targets TypeScript and TSX files only.

#!/bin/bash
# .claude/hooks/auto-format.sh
 
FILE=$(echo "$CLAUDE_TOOL_INPUT" | jq -r '.file_path // empty')
 
# Exit quietly if no file path was provided
[[ -z "$FILE" ]] && exit 0
 
# Apply Prettier to TypeScript files only
if [[ "$FILE" == *.ts || "$FILE" == *.tsx ]]; then
  npx prettier --write "$FILE" 2>/dev/null
  echo "Formatted: $FILE"
fi

The CLAUDE_TOOL_INPUT environment variable contains the tool's input as JSON. Use jq to pull out what you need.

2. Protect sensitive files (PreToolUse)

Block Claude from accidentally editing .env files, migration history, or generated code.

#!/bin/bash
# .claude/hooks/protect-files.sh
 
FILE=$(echo "$CLAUDE_TOOL_INPUT" | jq -r '.file_path // empty')
 
# List of path patterns to protect
PROTECTED=(
  ".env"
  ".env.local"
  ".env.production"
  "prisma/migrations"
  "generated/"
  "__generated__"
)
 
for pattern in "${PROTECTED[@]}"; do
  if [[ "$FILE" == *"$pattern"* ]]; then
    echo "BLOCKED: '$FILE' is a protected file. Edit manually if this is intentional." >&2
    exit 1
  fi
done
 
exit 0

Exiting with 1 stops the tool. Exiting with 0 lets it proceed.

3. Preserve context before compaction (PreCompact)

When a conversation grows long, Claude Code compacts older content. Save a snapshot before that happens so you can pick up where you left off.

#!/bin/bash
# .claude/hooks/save-context.sh
 
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
SAVE_DIR=".claude/snapshots"
mkdir -p "$SAVE_DIR"
 
# Write a context snapshot
cat > "$SAVE_DIR/context-$TIMESTAMP.md" << EOF
# Context Snapshot — $TIMESTAMP
 
## Git status
$(git status --short 2>/dev/null || echo "no git")
 
## Recently changed files
$(git diff --name-only HEAD 2>/dev/null | head -20 || echo "none")
 
## Notes
Auto-saved before compaction.
EOF
 
echo "Context saved to $SAVE_DIR/context-$TIMESTAMP.md"

4. Desktop notification when a task finishes (Notification)

You should not have to watch the terminal the whole time. Send a desktop notification when Claude is done.

#!/bin/bash
# .claude/hooks/notify-done.sh
 
# macOS
if command -v osascript &>/dev/null; then
  osascript -e 'display notification "Claude Code finished the task" with title "Claude Code" sound name "Glass"'
fi
 
# Linux (notify-send)
if command -v notify-send &>/dev/null; then
  notify-send "Claude Code" "Task complete"
fi

The Mental Model

Think of hooks as event-driven guardrails.

Claude Code exposes lifecycle points such as:

  • before a tool runs
  • after a tool runs
  • before compaction
  • after compaction

That gives you a way to inject your own checks without repeating the same prompt every session.

High-Value Uses

1. Protect critical files

Block accidental edits to files like:

  • .env
  • production deployment config
  • generated code
  • migration history

2. Auto-run checks

After edit-heavy operations, trigger:

  • eslint
  • tsc --noEmit
  • targeted tests
  • formatter checks

3. Preserve context

Before compaction, save useful state into:

  • task notes
  • summary files
  • progress artifacts

4. Send notifications

For long-running tasks, a hook can notify you when Claude is done instead of making you babysit the terminal.

Debugging Tips

When a hook does not behave as expected, try these approaches.

1. Run the script standalone

# Simulate the environment variable and run directly
export CLAUDE_TOOL_INPUT='{"file_path": "src/app.ts"}'
bash .claude/hooks/auto-format.sh
echo "Exit code: $?"

2. Log to stderr

Writing to >&2 inside a hook script surfaces output in Claude Code's error stream.

echo "DEBUG: FILE=$FILE" >&2

3. Check exit codes

A non-zero exit from a PreToolUse hook blocks the tool. If Claude is getting unexpectedly blocked, check what your script exits with.

4. Verify jq is installed

which jq || echo "jq not found — brew install jq or apt install jq"

Security Matters More Here

Anthropic is very explicit: hooks execute arbitrary shell commands on your system.

That means hooks are powerful, but also dangerous if written carelessly.

Security habits that matter:

  • quote shell variables
  • validate inputs
  • use absolute paths
  • block path traversal
  • skip secrets and sensitive files
  • test hooks in a safe environment first

A sloppy hook can be more dangerous than a sloppy prompt.

Complete Hook Event Reference

Here is a comprehensive table of every hook event type Claude Code supports.

Event Timing Use Case
PreToolUse Before tool executes Block dangerous operations, validate inputs
PostToolUse After tool executes Auto-format, lint, send notifications
PreCompact Before context compression Save important state, create snapshots
PostCompact After context compression Restore context, re-inject critical rules
Notification When Claude sends a notification Desktop alerts, Slack messages, sound effects
Stop When Claude finishes responding Session cleanup, final validation, auto-save

Key points:

  • Each event receives different environment variables
  • PreToolUse can BLOCK execution (exit 1). All others are advisory only
  • Matcher patterns support regex: "Edit|Write", "Bash", ".*" for all tools

Environment Variable Reference

These are the environment variables available inside hook scripts.

Variable Available In Contains
CLAUDE_TOOL_NAME All hooks Name of the tool being used (Edit, Write, Bash, etc.)
CLAUDE_TOOL_INPUT All hooks JSON with tool parameters (file_path, command, etc.)
CLAUDE_TOOL_OUTPUT PostToolUse only JSON with tool execution result
CLAUDE_SESSION_ID All hooks Current session identifier
CLAUDE_PROJECT_DIR All hooks Project root directory path
CLAUDE_MODEL All hooks Current model (claude-sonnet-4-6, etc.)
  • Parse JSON inputs with jq: echo "$CLAUDE_TOOL_INPUT" | jq -r '.file_path'
  • Always handle missing variables gracefully with defaults

PostCompact Hook Pattern

Context compression removes older conversation history to free up tokens. The problem is that important rules or context can disappear along with it.

The PostCompact hook runs after compression finishes and can re-inject critical context back into the conversation.

#!/bin/bash
# .claude/hooks/post-compact.sh
# Re-inject critical context after compression
 
cat << 'CONTEXT'
[Post-Compact Context Restoration]
- Current task: Review authentication module
- Important constraint: Do NOT modify database schema
- Active branch: feature/auth-refactor
- Files modified so far: src/auth/login.ts, src/auth/session.ts
CONTEXT

Register it in settings:

{
  "hooks": {
    "PostCompact": [
      {
        "matcher": ".*",
        "command": ".claude/hooks/post-compact.sh"
      }
    ]
  }
}
  • Output from PostCompact hooks is fed back into the conversation as context
  • Keep it short — long outputs defeat the purpose of compaction

Multi-Hook Workflow: Local CI Pipeline

Combine multiple hooks to build a complete local CI pipeline. Every time a file is edited, type checking, linting, and tests run automatically.

#!/bin/bash
# .claude/hooks/local-ci.sh — PostToolUse hook for Edit|Write
 
FILE=$(echo "$CLAUDE_TOOL_INPUT" | jq -r '.file_path // empty')
[[ -z "$FILE" ]] && exit 0
 
ERRORS=""
 
# Step 1: TypeScript type checking (only for .ts/.tsx files)
if [[ "$FILE" == *.ts || "$FILE" == *.tsx ]]; then
  if ! npx tsc --noEmit --pretty 2>/tmp/tsc-output.txt; then
    ERRORS+="TypeScript errors found:\n$(cat /tmp/tsc-output.txt)\n\n"
  fi
fi
 
# Step 2: ESLint check
if [[ "$FILE" == *.ts || "$FILE" == *.tsx || "$FILE" == *.js ]]; then
  if ! npx eslint "$FILE" --quiet 2>/tmp/eslint-output.txt; then
    ERRORS+="ESLint issues:\n$(cat /tmp/eslint-output.txt)\n\n"
  fi
fi
 
# Step 3: Run related tests
TEST_FILE="${FILE%.ts}.test.ts"
if [[ -f "$TEST_FILE" ]]; then
  if ! npx jest "$TEST_FILE" --silent 2>/tmp/test-output.txt; then
    ERRORS+="Test failures:\n$(cat /tmp/test-output.txt)\n\n"
  fi
fi
 
# Report results
if [[ -n "$ERRORS" ]]; then
  echo "⚠ Local CI found issues:" >&2
  echo -e "$ERRORS" >&2
else
  echo "✓ Local CI passed: types, lint, tests" >&2
fi
 
exit 0  # Don't block — just report

Key points:

  • This runs automatically after every file edit
  • Reports via stderr so Claude sees the feedback
  • exit 0 means advisory — Claude sees the warnings but is not blocked
  • Change to exit 1 in PreToolUse if you want to enforce

Complete settings.json for the full pipeline:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "command": ".claude/hooks/protect-files.sh"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": ".claude/hooks/auto-format.sh"
      },
      {
        "matcher": "Edit|Write",
        "command": ".claude/hooks/local-ci.sh"
      }
    ],
    "PreCompact": [
      {
        "matcher": ".*",
        "command": ".claude/hooks/save-context.sh"
      }
    ],
    "PostCompact": [
      {
        "matcher": ".*",
        "command": ".claude/hooks/post-compact.sh"
      }
    ],
    "Notification": [
      {
        "matcher": ".*",
        "command": ".claude/hooks/notify-done.sh"
      }
    ]
  }
}

Hooks vs CLAUDE.md

Tool Best for
CLAUDE.md policy, architecture, coding rules
hooks automatic enforcement and checks
prompt today's task

When Not to Use Hooks

Avoid hooks when the workflow is still changing quickly, when the commands are too slow to run constantly, or when the logic depends on nuanced human judgment.

Claude Code vs Codex

Codex usually leans on sandbox and approval boundaries. Claude Code hooks lean on local event-driven automation.

Connected Guides