Hooks System in Claude Code

Table of content

Hooks are shell commands that fire automatically at specific points in Claude Code’s lifecycle. Unlike prompts (which are suggestions), hooks are deterministic. They run every time, no exceptions.

Why Hooks Matter

Your AI assistant can read and write files. That’s powerful and dangerous. Hooks let you:

Hook Events

Five events trigger hooks:

EventWhen it fires
PreToolUseBefore any tool executes
PostToolUseAfter any tool completes
SessionStartWhen Claude Code launches
SessionEndWhen session terminates
NotificationWhen Claude sends a notification

Configuration

Add hooks to .claude/settings.json in your project:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "prettier --write \"$CLAUDE_FILE_PATH\""
      }
    ]
  }
}

The matcher field accepts regex patterns to filter which tools trigger the hook.

Environment Variables

Hooks receive context through environment variables:

Practical Examples

Auto-format After Edits

Run Prettier on JavaScript/TypeScript files after every edit:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "if [[ \"$CLAUDE_FILE_PATH\" =~ \\.(js|ts|jsx|tsx)$ ]]; then prettier --write \"$CLAUDE_FILE_PATH\"; fi"
      }
    ]
  }
}

Block Sensitive File Writes

Prevent writes to .env and production configs:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "if [[ \"$CLAUDE_FILE_PATH\" =~ (\\.env|production\\.config) ]]; then echo 'Blocked: sensitive file' >&2; exit 1; fi"
      }
    ]
  }
}

Exit code 1 aborts the operation.

Inject Context at Session Start

Load project-specific context when starting a session:

{
  "hooks": {
    "SessionStart": [
      {
        "command": "cat .claude/project-context.md"
      }
    ]
  }
}

Run Tests After Code Changes

Automatically run relevant tests:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "if [[ \"$CLAUDE_FILE_PATH\" =~ \\.py$ ]]; then pytest \"${CLAUDE_FILE_PATH%.*}_test.py\" 2>/dev/null || true; fi"
      }
    ]
  }
}

Go Format on Save

Auto-format Go files:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "if [[ \"$CLAUDE_FILE_PATH\" =~ \\.go$ ]]; then gofmt -w \"$CLAUDE_FILE_PATH\"; fi"
      }
    ]
  }
}

Debugging Hooks

Hooks fail silently by default. To debug:

  1. Add echo statements to trace execution
  2. Check exit codes explicitly
  3. Redirect stderr to a log file
{
  "command": "echo \"Hook fired: $CLAUDE_TOOL_NAME on $CLAUDE_FILE_PATH\" >> /tmp/claude-hooks.log"
}

Best Practices

Hooks vs MCP vs CLAUDE.md

MechanismPurposeDeterminism
HooksAutomated shell commandsAlways runs
MCPExternal tool integrationCalled when needed
CLAUDE.mdProject instructionsSuggestion only

Hooks enforce rules. CLAUDE.md suggests them.

Next: Building Custom MCP Servers

Topics: claude-code automation workflow