Claude Code Advanced
25 minutes
linux
macos
windows

Hooks System

Automate Claude Code workflows with pre and post hooks for file changes, commands, and sessions.

What you'll accomplish

Implement custom hooks to automate your Claude Code workflow.

Prerequisites

  • Completed Configuration Deep Dive lesson
  • Basic shell scripting knowledge

What Are Hooks?

Hooks are scripts that run automatically at specific points in Claude Code's workflow. They let you:

  • Run linting after every file change
  • Execute tests before committing
  • Format code automatically
  • Send notifications
  • Enforce coding standards

Imagine you have a rule: "Every time I put dishes in the sink, I also wipe the counter."

Hooks work the same way for Claude Code:

  • "Every time Claude writes a file, run the formatter"
  • "Before Claude runs a command, check if it's safe"
  • "After a session ends, create a summary"

The hook runs automatically without you having to remember.

Types of Hooks

Claude Code supports several hook types:

HookWhen It RunsUse Case
pre-writeBefore writing a fileValidate content, check permissions
post-writeAfter writing a fileFormat, lint, run tests
pre-executeBefore running a commandSecurity checks, logging
post-executeAfter running a commandParse output, notifications
pre-sessionWhen Claude Code startsSet up environment
post-sessionWhen Claude Code endsCleanup, summary
pre-promptBefore sending to ClaudeModify or enhance prompts

Configuring Hooks

Hooks are defined in your settings file:

json
{
  "hooks": {
    "post-write": {
      "command": "npm run lint:fix -- {{file}}",
      "patterns": ["*.ts", "*.tsx"],
      "enabled": true
    },
    "pre-execute": {
      "command": "./scripts/check-command.sh {{command}}",
      "enabled": true
    }
  }
}

Hook Configuration Options

OptionDescriptionRequired
commandThe command to runYes
patternsFile patterns to match (for file hooks)No
enabledWhether the hook is activeNo (default: true)
timeoutMax execution time in msNo (default: 30000)
cwdWorking directoryNo (default: project root)
failBehaviorWhat to do if hook failsNo (default: "warn")

Template Variables

Use these in your hook commands:

VariableAvailable InDescription
{{file}}File hooksPath to the file
{{files}}File hooksSpace-separated list of files
{{command}}Execute hooksThe command being run
{{output}}Post-executeCommand output
{{prompt}}Prompt hooksThe user's prompt
{{response}}Post-sessionClaude's last response

Common Hook Patterns

Auto-Format on Write

Run Prettier after every file change:

json
{
  "hooks": {
    "post-write": {
      "command": "npx prettier --write {{file}}",
      "patterns": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.json", "*.css"],
      "timeout": 10000
    }
  }
}

Lint After Changes

Run ESLint and show any issues:

json
{
  "hooks": {
    "post-write": {
      "command": "npx eslint {{file}} --fix",
      "patterns": ["*.ts", "*.tsx"],
      "failBehavior": "warn"
    }
  }
}

Run Related Tests

Run tests for files that changed:

json
{
  "hooks": {
    "post-write": {
      "command": "./scripts/run-related-tests.sh {{file}}",
      "patterns": ["src/**/*.ts"],
      "timeout": 60000
    }
  }
}

With a helper script (scripts/run-related-tests.sh):

bash
#!/bin/bash
FILE=$1

# Find related test file
TEST_FILE="${FILE%.ts}.test.ts"

if [ -f "$TEST_FILE" ]; then
  npx jest "$TEST_FILE" --passWithNoTests
fi

Type Check on Write

Run TypeScript compiler to check for errors:

json
{
  "hooks": {
    "post-write": {
      "command": "npx tsc --noEmit",
      "patterns": ["*.ts", "*.tsx"],
      "failBehavior": "warn"
    }
  }
}

Command Safety Check

Prevent dangerous commands:

json
{
  "hooks": {
    "pre-execute": {
      "command": "./scripts/check-command.sh {{command}}",
      "failBehavior": "block"
    }
  }
}

With safety script (scripts/check-command.sh):

bash
#!/bin/bash
COMMAND="$1"

# List of dangerous patterns
DANGEROUS=(
  "rm -rf /"
  "rm -rf ~"
  "rm -rf *"
  ":(){ :|:& };:"
  "dd if="
  "> /dev/sd"
  "mkfs"
  "--force"
  "-f origin main"
)

for pattern in "${DANGEROUS[@]}"; do
  if [[ "$COMMAND" == *"$pattern"* ]]; then
    echo "BLOCKED: Command contains dangerous pattern: $pattern"
    exit 1
  fi
done

echo "Command approved: $COMMAND"
exit 0

Session Summary

Generate a summary when the session ends:

json
{
  "hooks": {
    "post-session": {
      "command": "./scripts/log-session.sh",
      "enabled": true
    }
  }
}

With logging script (scripts/log-session.sh):

bash
#!/bin/bash
DATE=$(date +"%Y-%m-%d %H:%M:%S")
LOG_DIR="$HOME/.claude/logs"
mkdir -p "$LOG_DIR"

echo "[$DATE] Session ended in $(pwd)" >> "$LOG_DIR/sessions.log"
echo "Files changed:" >> "$LOG_DIR/sessions.log"
git diff --name-only HEAD 2>/dev/null >> "$LOG_DIR/sessions.log"
echo "---" >> "$LOG_DIR/sessions.log"

Advanced Hook Examples

Notify on Completion

Send a notification when Claude finishes a long task:

Auto-Commit Changes

Commit changes after each file write:

json
{
  "hooks": {
    "post-write": {
      "command": "./scripts/auto-commit.sh {{file}}",
      "patterns": ["*.ts", "*.tsx"]
    }
  }
}

With commit script (scripts/auto-commit.sh):

bash
#!/bin/bash
FILE=$1

# Only commit if there are changes
if [ -n "$(git status --porcelain $FILE)" ]; then
  git add "$FILE"
  git commit -m "Auto-commit: Update $FILE"
fi
Warning

Auto-committing can create noisy Git history. Use sparingly, and consider squashing commits later.

Enforce File Structure

Block writes to forbidden locations:

json
{
  "hooks": {
    "pre-write": {
      "command": "./scripts/check-file-path.sh {{file}}",
      "failBehavior": "block"
    }
  }
}

With path checker (scripts/check-file-path.sh):

bash
#!/bin/bash
FILE=$1

# Block writes outside the src directory
if [[ ! "$FILE" == src/* && ! "$FILE" == tests/* && ! "$FILE" == *.md ]]; then
  echo "BLOCKED: Can only write to src/, tests/, or markdown files"
  exit 1
fi

# Block writes to generated files
if [[ "$FILE" == *generated* || "$FILE" == *dist/* ]]; then
  echo "BLOCKED: Cannot modify generated files"
  exit 1
fi

exit 0

Dynamic Prompting

Enhance prompts with project context:

json
{
  "hooks": {
    "pre-prompt": {
      "command": "./scripts/enhance-prompt.sh"
    }
  }
}

This is an advanced pattern where your script outputs additional context that gets prepended to the user's prompt.

Fail Behaviors

The failBehavior option controls what happens when a hook fails:

ValueBehavior
"ignore"Continue silently
"warn"Show warning but continue
"block"Stop the operation
"retry"Try the hook again (up to 3 times)

Choose based on the hook's importance:

  • Formatting: "warn" (nice to have)
  • Security checks: "block" (critical)
  • Tests: "warn" or "block" (depends on workflow)

Debugging Hooks

Enable Verbose Mode

See hook execution details:

bash
claude --verbose

Test Hooks Manually

Run your hook script directly to debug:

bash
./scripts/check-command.sh "npm install"
echo $?  # Check exit code

Check Hook Logs

Claude Code logs hook execution. Check:

bash
cat ~/.claude/logs/hooks.log

Common Issues

Frequently Asked Questions

Check:

  1. Hook is enabled: true (or omitted - defaults to true)
  2. File pattern matches (use echo {{file}} to debug)
  3. Script is executable (chmod +x script.sh)
  4. Settings file is valid JSON
  1. Test the command manually in terminal
  2. Check that paths are correct (use absolute paths if needed)
  3. Verify template variables are expanding correctly
  4. Check the script's exit code

Check:

  1. Script exit code (0 = success, non-zero = failure)
  2. failBehavior setting
  3. Error output from the script
  1. Add a timeout value
  2. Run expensive operations asynchronously (background with &)
  3. Only match specific patterns (don't run on every file)
  4. Cache results where possible

Best Practices

1. Keep Hooks Fast

Hooks run synchronously. Slow hooks make Claude Code feel sluggish. Target under 5 seconds for post-write hooks.

2. Use Exit Codes Correctly

  • exit 0 = success
  • exit 1 = failure (will trigger failBehavior)

3. Be Specific with Patterns

Instead of running on all files:

json
"patterns": ["*.ts", "*.tsx"]

Not:

json
"patterns": ["*"]

4. Log for Debugging

Write to a log file in complex hooks:

bash
echo "[$(date)] Hook ran on $FILE" >> ~/.claude/hooks.log

5. Test Before Deploying

Test hooks locally before adding to project settings. A broken hook affects everyone.

Verify Your Understanding

You've completed this lesson if you can:

0/5

Next: Understanding Claude Code's permission system and when to use --dangerously-skip-permissions.