Claude Code Hooks Tutorial: Automate Your AI Coding Workflow

Learn how to use Claude Code hooks to automate tasks, enforce coding standards, and create powerful custom workflows. Complete tutorial with practical examples for PreToolUse, PostToolUse, and Notification hooks.

ClaudeCode.Guide Team
hooksautomationtutorialworkflowadvanced

Claude Code Hooks Tutorial: Automate Your AI Coding Workflow

Hooks are one of the most powerful features in Claude Code, allowing you to automate tasks, enforce coding standards, and create custom workflows. This comprehensive tutorial covers everything from basic setup to advanced use cases.

What Are Claude Code Hooks?

Hooks are shell commands that automatically execute at specific points during Claude Code's operation. They enable you to:

  • Automate repetitive tasks - Run formatters, linters, or tests automatically
  • Enforce project standards - Block certain actions or require specific patterns
  • Extend functionality - Integrate with external tools and services
  • Create guardrails - Add safety checks before destructive operations

Hook Types Overview

Claude Code supports four types of hooks:

Hook TypeWhen It RunsUse Cases
PreToolUseBefore a tool executesValidation, approval, blocking
PostToolUseAfter a tool completesFormatting, notifications, logging
NotificationWhen Claude sends notificationsAlerts, integrations, logging
StopWhen Claude finishes a taskCleanup, verification, reporting

Setting Up Hooks

Hooks are configured in your .claude/settings.json file:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit",
        "command": "echo 'About to edit: $CLAUDE_FILE_PATH'"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit",
        "command": "npx prettier --write $CLAUDE_FILE_PATH"
      }
    ]
  }
}

Configuration Location

Hooks can be defined in:

  1. Project-level: .claude/settings.json in your project root
  2. User-level: ~/.claude/settings.json for global hooks
  3. CLAUDE.md: Inline hook definitions (limited)

PreToolUse Hooks

PreToolUse hooks run before Claude executes a tool. They can:

  • Log what's about to happen
  • Block operations by returning non-zero exit codes
  • Modify behavior based on conditions

Basic Example: Logging All Edits

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit",
        "command": "echo \"[$(date)] Editing: $CLAUDE_FILE_PATH\" >> ~/.claude/edit.log"
      }
    ]
  }
}

Blocking Dangerous Operations

Prevent edits to critical files:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit",
        "command": "if [[ \"$CLAUDE_FILE_PATH\" == *\".env\"* ]]; then echo 'Cannot edit .env files!' && exit 1; fi"
      }
    ]
  }
}

Requiring Confirmation for Destructive Commands

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "if [[ \"$CLAUDE_COMMAND\" == *\"rm -rf\"* ]]; then echo 'Blocked: rm -rf is not allowed' && exit 1; fi"
      }
    ]
  }
}

Pattern Matching

The matcher field supports:

  • Exact match: "Edit", "Bash", "Write"
  • Wildcard: "*" matches all tools
  • Regex: "/Edit|Write/" matches Edit or Write

PostToolUse Hooks

PostToolUse hooks run after a tool completes successfully. Perfect for:

  • Auto-formatting code
  • Running linters
  • Updating documentation
  • Sending notifications

Auto-Format After Edits

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "command": "npx prettier --write \"$CLAUDE_FILE_PATH\" 2>/dev/null || true"
      },
      {
        "matcher": "Write",
        "command": "npx prettier --write \"$CLAUDE_FILE_PATH\" 2>/dev/null || true"
      }
    ]
  }
}

Run ESLint After JavaScript Changes

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "command": "if [[ \"$CLAUDE_FILE_PATH\" == *.js ]] || [[ \"$CLAUDE_FILE_PATH\" == *.ts ]]; then npx eslint --fix \"$CLAUDE_FILE_PATH\"; fi"
      }
    ]
  }
}

Auto-Run Tests

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "command": "if [[ \"$CLAUDE_FILE_PATH\" == *\"test\"* ]]; then npm test -- --findRelatedTests \"$CLAUDE_FILE_PATH\"; fi"
      }
    ]
  }
}

Notification Hooks

Notification hooks trigger when Claude sends a notification (task completion, errors, etc.).

Desktop Notifications (macOS)

{
  "hooks": {
    "Notification": [
      {
        "matcher": "*",
        "command": "osascript -e 'display notification \"$CLAUDE_MESSAGE\" with title \"Claude Code\"'"
      }
    ]
  }
}

Desktop Notifications (Linux)

{
  "hooks": {
    "Notification": [
      {
        "matcher": "*",
        "command": "notify-send 'Claude Code' \"$CLAUDE_MESSAGE\""
      }
    ]
  }
}

Slack Integration

{
  "hooks": {
    "Notification": [
      {
        "matcher": "*",
        "command": "curl -X POST -H 'Content-type: application/json' --data '{\"text\":\"Claude Code: '$CLAUDE_MESSAGE'\"}' $SLACK_WEBHOOK_URL"
      }
    ]
  }
}

Stop Hooks

Stop hooks run when Claude completes a task or conversation turn.

Generate Summary Report

{
  "hooks": {
    "Stop": [
      {
        "matcher": "*",
        "command": "echo \"Task completed at $(date)\" >> ~/.claude/completed.log"
      }
    ]
  }
}

Auto-Commit on Completion

{
  "hooks": {
    "Stop": [
      {
        "matcher": "*",
        "command": "if [ -n \"$(git status --porcelain)\" ]; then git add -A && git commit -m 'Auto-commit: Claude Code changes'; fi"
      }
    ]
  }
}

Environment Variables

Hooks have access to these environment variables:

VariableDescriptionAvailable In
CLAUDE_FILE_PATHPath of file being editedEdit, Write, Read
CLAUDE_TOOL_NAMEName of the current toolAll hooks
CLAUDE_COMMANDBash command being runBash tool
CLAUDE_MESSAGENotification messageNotification hooks
CLAUDE_SESSION_IDCurrent session IDAll hooks
CLAUDE_WORKING_DIRCurrent working directoryAll hooks

Advanced Hook Patterns

Conditional Hooks Based on File Type

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "command": "case \"$CLAUDE_FILE_PATH\" in *.py) black \"$CLAUDE_FILE_PATH\";; *.js|*.ts) npx prettier --write \"$CLAUDE_FILE_PATH\";; *.go) gofmt -w \"$CLAUDE_FILE_PATH\";; esac"
      }
    ]
  }
}

Chain Multiple Actions

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "command": "npx prettier --write \"$CLAUDE_FILE_PATH\" && npx eslint --fix \"$CLAUDE_FILE_PATH\" && echo 'Formatted and linted!'"
      }
    ]
  }
}

Using External Scripts

Create a hook script ~/.claude/hooks/post-edit.sh:

#!/bin/bash
FILE="$CLAUDE_FILE_PATH"

# Format based on extension
case "${FILE##*.}" in
  py)
    black "$FILE"
    isort "$FILE"
    ;;
  js|ts|jsx|tsx)
    npx prettier --write "$FILE"
    npx eslint --fix "$FILE"
    ;;
  go)
    gofmt -w "$FILE"
    ;;
esac

# Log the edit
echo "[$(date)] Edited: $FILE" >> ~/.claude/edits.log

Reference it in settings:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "command": "~/.claude/hooks/post-edit.sh"
      }
    ]
  }
}

Project-Specific Hooks

In your project's .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "if [[ \"$CLAUDE_COMMAND\" == *\"npm publish\"* ]]; then echo 'Run tests first!' && npm test || exit 1; fi"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit",
        "command": "npm run lint:fix -- \"$CLAUDE_FILE_PATH\""
      }
    ],
    "Stop": [
      {
        "matcher": "*",
        "command": "npm run typecheck"
      }
    ]
  }
}

Debugging Hooks

Enable Verbose Logging

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "command": "echo \"[DEBUG] Tool: $CLAUDE_TOOL_NAME, File: $CLAUDE_FILE_PATH\" >> /tmp/claude-hooks.log"
      }
    ]
  }
}

Test Hooks Manually

# Simulate environment variables
export CLAUDE_FILE_PATH="/path/to/test.js"
export CLAUDE_TOOL_NAME="Edit"

# Run your hook command
npx prettier --write "$CLAUDE_FILE_PATH"

Common Hook Recipes

1. Enforce Branch Protection

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "BRANCH=$(git branch --show-current); if [[ \"$BRANCH\" == \"main\" || \"$BRANCH\" == \"master\" ]]; then echo 'Cannot run commands on protected branch!' && exit 1; fi"
      }
    ]
  }
}

2. Auto-Update Tests

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "command": "TEST_FILE=$(echo \"$CLAUDE_FILE_PATH\" | sed 's/\\.ts$/.test.ts/'); if [ -f \"$TEST_FILE\" ]; then echo \"Remember to update: $TEST_FILE\"; fi"
      }
    ]
  }
}

3. Security Scanning

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "command": "if grep -q 'password\\|secret\\|api_key' \"$CLAUDE_FILE_PATH\"; then echo 'WARNING: Possible sensitive data detected!'; fi"
      }
    ]
  }
}

4. Documentation Reminder

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "if [[ \"$CLAUDE_FILE_PATH\" == *\"/src/\"* ]]; then echo 'Remember to update documentation if this is a public API!'; fi"
      }
    ]
  }
}

Best Practices

  1. Keep hooks fast - Slow hooks will slow down your entire workflow
  2. Handle errors gracefully - Use || true for non-critical hooks
  3. Use exit codes correctly - Return non-zero to block operations in PreToolUse
  4. Log important events - Create an audit trail for team projects
  5. Test hooks thoroughly - Bad hooks can break your workflow
  6. Document project hooks - Help team members understand custom behaviors

Troubleshooting

Hook Not Running

  1. Check the matcher pattern matches the tool name exactly
  2. Verify the command syntax is correct
  3. Check file permissions for external scripts
  4. Look for typos in settings.json

Hook Blocks Everything

  1. Check your PreToolUse exit codes
  2. Verify conditional logic is correct
  3. Test the command manually

Performance Issues

  1. Move heavy operations to PostToolUse or Stop hooks
  2. Use async operations where possible
  3. Consider using background processes: command &

Last updated: January 2025