Hooks System Intermediate

Hooks let you run shell commands automatically when specific events occur in Claude Code. They are the simplest way to extend Claude Code's behavior without writing complex integrations.

Hook Types

Hook When It Fires Use Cases
preToolUse Before Claude executes a tool Validation, blocking dangerous operations, logging
postToolUse After Claude executes a tool Auto-formatting, linting, notifications
afterMessage After Claude sends a complete response Logging, analytics, follow-up actions
notification When Claude generates a notification Desktop notifications, Slack alerts

Hook Configuration

Hooks are configured in your Claude Code settings file. You can set them at the user or project level:

~/.claude/settings.json (User-level)
{
  "hooks": {
    "postToolUse": [
      {
        "matcher": "Write",
        "command": "npx prettier --write \"$CLAUDE_FILE_PATH\""
      }
    ]
  }
}
.claude/settings.json (Project-level)
{
  "hooks": {
    "postToolUse": [
      {
        "matcher": "Write",
        "command": "npx eslint --fix \"$CLAUDE_FILE_PATH\""
      },
      {
        "matcher": "Edit",
        "command": "npx eslint --fix \"$CLAUDE_FILE_PATH\""
      }
    ],
    "afterMessage": [
      {
        "command": "echo \"$(date): Claude responded\" >> .claude/activity.log"
      }
    ],
    "notification": [
      {
        "command": "notify-send 'Claude Code' \"$CLAUDE_NOTIFICATION\""
      }
    ]
  }
}

Matchers

Matchers filter which tool invocations trigger a hook. Without a matcher, the hook fires for every tool use:

Matcher Examples
# Match a specific tool
"matcher": "Write"          → fires only for the Write tool
"matcher": "Edit"           → fires only for the Edit tool
"matcher": "Bash"           → fires only for the Bash tool

# Match file patterns (in the tool's arguments)
"matcher": "*.ts"           → fires for TypeScript files
"matcher": "*.py"           → fires for Python files
"matcher": "src/**/*.js"    → fires for JS files in src/

Practical Examples

Auto-Format After Edit

JSON
{
  "hooks": {
    "postToolUse": [
      {
        "matcher": "Write",
        "command": "npx prettier --write \"$CLAUDE_FILE_PATH\""
      },
      {
        "matcher": "Edit",
        "command": "npx prettier --write \"$CLAUDE_FILE_PATH\""
      }
    ]
  }
}

Lint After Code Changes

JSON
{
  "hooks": {
    "postToolUse": [
      {
        "matcher": "Write",
        "command": "npx eslint \"$CLAUDE_FILE_PATH\" --fix --quiet 2>/dev/null || true"
      }
    ]
  }
}

Desktop Notifications

JSON
{
  "hooks": {
    "notification": [
      {
        "command": "osascript -e 'display notification \"$CLAUDE_NOTIFICATION\" with title \"Claude Code\"'"
      }
    ]
  }
}

Activity Logging

JSON
{
  "hooks": {
    "postToolUse": [
      {
        "command": "echo \"$(date -Iseconds) | Tool: $CLAUDE_TOOL_NAME | File: $CLAUDE_FILE_PATH\" >> /tmp/claude-activity.log"
      }
    ]
  }
}
Performance Note: Hooks run synchronously - Claude Code waits for each hook to complete before continuing. Keep hook commands fast (under 2 seconds). For slow operations, run them in the background with &.
Debugging Hooks: If a hook is not working, check:
  • The command works when run manually in the terminal
  • The matcher string matches the expected tool name exactly
  • Environment variables like $CLAUDE_FILE_PATH are available
  • The settings.json is valid JSON (use a JSON validator)

Ready to Go Deeper?

Live instructor-led courses from our partners. Affiliate disclosure.