Skip to content
Back to Blog
Agentic AI6 min read

Claude Code Hooks: Automating Your Development Workflow

Deep dive into Claude Code hooks — pre and post tool execution hooks that let you enforce linting, run tests automatically, validate changes, and build custom CI-like workflows.

What Are Claude Code Hooks?

Claude Code hooks are user-defined shell commands that execute automatically at specific points during Claude Code's agentic workflow. They let you inject custom logic before or after Claude Code performs actions — similar to git hooks, but for AI-assisted development.

Hooks solve a fundamental problem: you want Claude Code to follow specific procedures (run linting after every edit, validate JSON schemas, check for secrets) but you do not want to repeat these instructions in every conversation. Hooks make these procedures automatic and enforceable.

Hook Types

Claude Code supports hooks at several execution points:

PreToolUse Hooks

PreToolUse hooks run before Claude Code executes a tool. They can inspect the planned action and either allow it, modify it, or block it.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit",
        "hook": "python3 .claude/hooks/pre-edit-check.py"
      }
    ]
  }
}

Use cases:

  • Block edits to protected files — Prevent Claude from modifying migration files, lock files, or generated code
  • Validate before write — Check that new files follow naming conventions
  • Security scanning — Scan Bash commands for dangerous operations before execution

PostToolUse Hooks

PostToolUse hooks run after a tool completes. They can inspect the result and trigger follow-up actions.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hook": "npx eslint --fix $CLAUDE_FILE_PATH"
      },
      {
        "matcher": "Write",
        "hook": "npx prettier --write $CLAUDE_FILE_PATH"
      }
    ]
  }
}

Use cases:

  • Auto-format after edits — Run Prettier, Black, or gofmt on every modified file
  • Lint checking — Run ESLint or Ruff after file changes
  • Test execution — Automatically run relevant tests after code changes
  • Schema validation — Validate JSON/YAML files after writing

Notification Hooks

Notification hooks trigger when specific events occur, such as Claude Code requesting user input or completing a long task.

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hook": "terminal-notifier -message '$CLAUDE_NOTIFICATION' -title 'Claude Code'"
      }
    ]
  }
}

Configuring Hooks

Hooks are defined in .claude/settings.json at the project level or ~/.claude/settings.json globally.

Full Configuration Example

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hook": "python3 .claude/hooks/validate-bash-command.py",
        "timeout": 5000
      },
      {
        "matcher": "Edit|Write",
        "hook": ".claude/hooks/check-protected-files.sh"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hook": ".claude/hooks/post-edit.sh"
      },
      {
        "matcher": "Write",
        "hook": ".claude/hooks/post-write.sh"
      }
    ],
    "Notification": [
      {
        "matcher": "",
        "hook": "notify-send 'Claude Code' '$CLAUDE_NOTIFICATION'"
      }
    ]
  }
}

Matcher Patterns

The matcher field determines which tool triggers the hook. It supports:

  • Exact match: "Edit" — only Edit tool calls
  • Pipe-separated alternatives: "Edit|Write" — both Edit and Write
  • Empty string: "" — matches all tools/events

Environment Variables Available to Hooks

Variable Description
$CLAUDE_TOOL_NAME The tool being called (Read, Edit, Write, Bash, etc.)
$CLAUDE_FILE_PATH The file being operated on (for file tools)
$CLAUDE_BASH_COMMAND The command being executed (for Bash tool)
$CLAUDE_NOTIFICATION The notification message (for Notification hooks)
$CLAUDE_PROJECT_DIR The project root directory

Practical Hook Recipes

Recipe 1: Auto-Format on Every Edit

#!/bin/bash
# .claude/hooks/post-edit.sh
FILE="$CLAUDE_FILE_PATH"

case "$FILE" in
  *.ts|*.tsx|*.js|*.jsx)
    npx prettier --write "$FILE" 2>/dev/null
    npx eslint --fix "$FILE" 2>/dev/null
    ;;
  *.py)
    ruff format "$FILE" 2>/dev/null
    ruff check --fix "$FILE" 2>/dev/null
    ;;
  *.go)
    gofmt -w "$FILE" 2>/dev/null
    ;;
  *.rs)
    rustfmt "$FILE" 2>/dev/null
    ;;
esac

This hook auto-formats every file Claude Code edits, ensuring consistent style without Claude needing to worry about formatting.

Recipe 2: Protect Critical Files

#!/bin/bash
# .claude/hooks/check-protected-files.sh

PROTECTED_PATTERNS=(
  "*.lock"
  "package-lock.json"
  "yarn.lock"
  "migrations/versions/*.py"
  ".env*"
  "*.pem"
  "*.key"
)

for pattern in "${PROTECTED_PATTERNS[@]}"; do
  if [[ "$CLAUDE_FILE_PATH" == $pattern ]]; then
    echo "BLOCKED: Cannot modify protected file: $CLAUDE_FILE_PATH"
    exit 1
  fi
done

exit 0

When a PreToolUse hook exits with a non-zero code, Claude Code blocks the tool execution and shows the hook's output to the model, which then adjusts its approach.

Recipe 3: Run Tests After Changes

#!/bin/bash
# .claude/hooks/post-edit-test.sh

FILE="$CLAUDE_FILE_PATH"

# Find and run related test files
if [[ "$FILE" == *.py ]]; then
  TEST_FILE="${FILE/app\//tests/test_}"
  if [[ -f "$TEST_FILE" ]]; then
    pytest "$TEST_FILE" -x --tb=short -q 2>&1 | tail -5
  fi
elif [[ "$FILE" == *.ts || "$FILE" == *.tsx ]]; then
  TEST_FILE="${FILE%.ts*}.test${FILE##*.ts}"
  if [[ -f "$TEST_FILE" ]]; then
    npx vitest run "$TEST_FILE" --reporter=verbose 2>&1 | tail -10
  fi
fi

Recipe 4: Secret Detection

#!/bin/bash
# .claude/hooks/scan-secrets.sh

FILE="$CLAUDE_FILE_PATH"

# Skip binary files and known safe patterns
if file "$FILE" | grep -q "binary"; then
  exit 0
fi

# Check for common secret patterns
PATTERNS=(
  "sk-[a-zA-Z0-9]{20,}"
  "AKIA[0-9A-Z]{16}"
  "ghp_[a-zA-Z0-9]{36}"
  "-----BEGIN (RSA |EC )?PRIVATE KEY-----"
  "password\s*=\s*["'][^"']{8,}["']"
)

for pattern in "${PATTERNS[@]}"; do
  if grep -qP "$pattern" "$FILE" 2>/dev/null; then
    echo "BLOCKED: Potential secret detected in $FILE matching pattern: $pattern"
    exit 1
  fi
done

exit 0

Hook Execution Order and Error Handling

Execution Order

  1. PreToolUse hooks run sequentially in the order they are defined
  2. If any PreToolUse hook exits non-zero, the tool call is blocked
  3. If all PreToolUse hooks pass, the tool executes
  4. PostToolUse hooks run sequentially after the tool completes
  5. PostToolUse hook failures are reported but do not undo the tool execution

Timeout Handling

Hooks have a configurable timeout (default: 60 seconds). If a hook exceeds its timeout, it is killed and treated as a failure for PreToolUse (blocks the action) or a warning for PostToolUse.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hook": "npm test -- --timeout 30000",
        "timeout": 45000
      }
    ]
  }
}

Hook Output

Hooks can write to stdout and stderr. This output is captured and fed back to Claude Code's model, allowing it to react to hook results. For example, if a linting hook reports errors, Claude Code will see those errors and can fix them in its next tool call.

Hooks vs CLAUDE.md Instructions

Both hooks and CLAUDE.md instructions influence Claude Code's behavior, but they work differently:

Aspect CLAUDE.md Hooks
Enforcement Advisory (model follows them but can deviate) Mandatory (PreToolUse hooks block execution)
Execution Interpreted by the AI model Executed as shell commands
Timing Read at session start Run at each tool call
Reliability High but not guaranteed Guaranteed (scripts run regardless)

Use CLAUDE.md for: coding conventions, architecture guidelines, style preferences

Use hooks for: formatting enforcement, security scanning, file protection, automated testing

The combination is powerful: CLAUDE.md tells Claude Code how to write code, and hooks verify that the code meets your standards after every change.

Team Workflow with Hooks

When your hooks are committed to the repository in .claude/settings.json and .claude/hooks/, every team member who uses Claude Code gets the same automated checks. This creates a consistent development experience:

  1. A developer asks Claude Code to implement a feature
  2. Claude Code writes the code
  3. PostToolUse hooks automatically format it and run linting
  4. If linting fails, Claude Code sees the errors and fixes them
  5. The developer reviews clean, formatted, validated code

This is essentially a local CI pipeline that runs on every AI-generated edit.

Conclusion

Claude Code hooks transform your AI coding assistant from a tool that follows suggestions into one that enforces standards. By combining PreToolUse hooks (for protection and validation) with PostToolUse hooks (for formatting and testing), you create guardrails that ensure Claude Code's output meets your team's quality bar automatically, every time.

Share this article
N

NYC News

Expert insights on AI voice agents and customer communication automation.

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.