Files
claude-plugins/claude-code/skills/claude-code-hooks/SKILL.md
movq 7911d90995 feat: Convert to Claude Code plugin marketplace
Transform repository into a plugin marketplace structure with two plugins:

- claude-code plugin: Complete toolkit with 5 skills
  * claude-code-plugins
  * claude-code-slash-commands
  * claude-code-hooks
  * claude-code-subagents
  * claude-code-memory

- claude-skills plugin: Meta-skill for creating Agent Skills
  * Comprehensive best practices guide
  * Templates and examples
  * Progressive disclosure patterns

Infrastructure:
- Add marketplace.json manifest
- Create plugin.json for each plugin
- Update documentation for marketplace structure
- Add contribution and testing guides

Installation:
- /plugin install claude-code@claude-skills
- /plugin install claude-skills@claude-skills

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 11:17:09 -05:00

18 KiB

name, description
name description
Claude Code Hooks Configure event-driven hooks for Claude Code to automate workflows, validate code, inject context, and control tool execution. Use when setting up automation, enforcing standards, or integrating external tools with Claude Code.

Claude Code Hooks

When to Use This Skill

Use this skill when:

  • Automating workflows with event-driven triggers
  • Enforcing code standards or security policies
  • Validating changes before/after tool execution
  • Injecting context at session start
  • Logging or monitoring tool usage
  • Controlling MCP tool permissions
  • Setting up team-wide automation

Do NOT use this skill for:

  • Creating slash commands (use claude-code-slash-commands skill)
  • Building full plugins (use claude-code-plugins skill)
  • One-time scripts (just run them directly)

Quick Start: Creating a Hook

1. Basic Hook Configuration

Create or edit settings file:

# Project-wide (team shared)
.claude/settings.json

# User-specific (not shared)
.claude/settings.local.json

# Global (all projects)
~/.claude/settings.json

2. Simple Example - Log File Changes

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"$(date): Modified $TOOL_ARGS\" >> .claude/changes.log"
          }
        ]
      }
    ]
  }
}

Result: Every file write/edit gets logged with timestamp.

Hook Types

SessionStart

Runs when a session begins (before any interaction):

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Project: MyApp | Env: production' > .claude/context.txt"
          }
        ]
      }
    ]
  }
}

Use For: Injecting project context, loading environment info

SessionEnd

Runs when a session ends:

{
  "hooks": {
    "SessionEnd": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "git add . && git stash save 'Claude session backup'"
          }
        ]
      }
    ]
  }
}

Use For: Cleanup, backups, state preservation

PreToolUse

Runs after Claude creates tool parameters but before execution:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'About to write: $TOOL_ARGS' && exit 0"
          }
        ]
      }
    ]
  }
}

Use For: Validation, permission checks, preprocessing

PostToolUse

Runs immediately after tool completes successfully:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write $TOOL_ARGS"
          }
        ]
      }
    ]
  }
}

Use For: Formatting, linting, post-processing

UserPromptSubmit

Runs when user submits a prompt (before Claude processes it):

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "date >> .claude/prompt-log.txt && exit 0"
          }
        ]
      }
    ]
  }
}

Use For: Logging, analytics, context injection

Notification

Runs when Claude sends notifications (permission requests, idle waiting):

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

Use For: Desktop notifications, alerts

Stop / SubagentStop

Runs when agents finish responding:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Response complete' >> .claude/activity.log && exit 0"
          }
        ]
      }
    ]
  }
}

Use For: Post-response processing, logging

PreCompact

Runs before context compaction:

{
  "hooks": {
    "PreCompact": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Compacting context...' && exit 0"
          }
        ]
      }
    ]
  }
}

Use For: Logging, saving state before compaction

Matcher Patterns

Tool Name Matching

// Single tool
"matcher": "Write"

// Multiple tools (OR)
"matcher": "Write|Edit|Read"

// All tools
"matcher": "*"

// Regex patterns supported
"matcher": "Bash.*|Git.*"

Common Patterns

// File operations
"matcher": "Write|Edit|Delete"

// Read operations
"matcher": "Read|Grep|Glob"

// Git operations
"matcher": "Bash(git:*)"

// All Bash commands
"matcher": "Bash"

Hook Configuration

Basic Structure

{
  "type": "command",
  "command": "your-command-here",
  "timeout": 60
}

Command Field

Use bash commands with environment variables:

{
  "type": "command",
  "command": "cd $CLAUDE_PROJECT_DIR && npm test"
}

Timeout Field

Optional execution limit in seconds (default: 60):

{
  "type": "command",
  "command": "long-running-task",
  "timeout": 300  // 5 minutes
}

Exit Codes & Control Flow

Exit Code 0 - Success (Continue)

# Hook succeeds, stdout shown to user (most events)
echo "Success message"
exit 0

Exit Code 2 - Blocking Error

# Hook blocks operation, stderr sent to Claude
echo "Error: Cannot proceed" >&2
exit 2

Use For: Validation failures, permission denials

Other Exit Codes - Non-Blocking Error

# Hook fails but operation continues
echo "Warning: Hook failed" >&2
exit 1

Environment Variables

Available Variables

$CLAUDE_PROJECT_DIR   # Current project directory
$TOOL_NAME           # Name of the tool being used
$TOOL_ARGS           # Arguments passed to the tool
$HOOK_EVENT          # Event type (PreToolUse, PostToolUse, etc.)

Example Usage

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "cd $CLAUDE_PROJECT_DIR && git add $TOOL_ARGS"
          }
        ]
      }
    ]
  }
}

Complete Examples

1. Auto-Format on Save

Format files automatically after writing/editing:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "cd $CLAUDE_PROJECT_DIR && npx prettier --write $TOOL_ARGS && exit 0"
          }
        ]
      }
    ]
  }
}

2. Security Validation

Block writes containing secrets:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "if grep -qiE '(password|api[_-]?key|secret)' $TOOL_ARGS 2>/dev/null; then echo 'Error: Possible secret detected' >&2; exit 2; fi; exit 0",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

3. Auto-Git on Changes

Automatically stage and commit changes:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "cd $CLAUDE_PROJECT_DIR && git add $TOOL_ARGS && git commit -m 'Auto-commit: Modified $TOOL_ARGS' && exit 0"
          }
        ]
      }
    ]
  }
}

4. Inject Project Context

Load project info at session start:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "cat << EOF\nProject: MyApp\nEnvironment: Production\nKey Files: src/config.js, .env.example\nCoding Standards: See CONTRIBUTING.md\nEOF"
          }
        ]
      }
    ]
  }
}

5. Test Before Commit

Run tests before allowing file writes:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "cd $CLAUDE_PROJECT_DIR && npm test -- --silent || (echo 'Tests failed, cannot save' >&2; exit 2)",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

6. Log All Activity

Track all tool usage:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"$(date '+%Y-%m-%d %H:%M:%S') - $TOOL_NAME - $TOOL_ARGS\" >> $CLAUDE_PROJECT_DIR/.claude/activity.log && exit 0"
          }
        ]
      }
    ]
  }
}

7. Enforce File Permissions

Ensure proper permissions on sensitive files:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "if [[ $TOOL_ARGS == *\".env\"* ]]; then chmod 600 $TOOL_ARGS; fi; exit 0"
          }
        ]
      }
    ]
  }
}

8. Notify on Long Sessions

Alert when session ends:

{
  "hooks": {
    "SessionEnd": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Session ended\" with title \"Claude Code\"' && exit 0"
          }
        ]
      }
    ]
  }
}

Advanced Patterns

Multiple Hooks per Event

Run several hooks in sequence:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write $TOOL_ARGS"
          },
          {
            "type": "command",
            "command": "npx eslint --fix $TOOL_ARGS"
          },
          {
            "type": "command",
            "command": "git add $TOOL_ARGS"
          }
        ]
      }
    ]
  }
}

Hooks run in order; if one fails, subsequent hooks may not run.

Conditional Execution

Use bash conditionals:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "if [[ $TOOL_ARGS == *.js ]]; then npm run format $TOOL_ARGS; fi; exit 0"
          }
        ]
      }
    ]
  }
}

Script-Based Hooks

Call external scripts for complex logic:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-write.sh $TOOL_ARGS",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

validate-write.sh:

#!/bin/bash
file=$1

# Check file size
if [ -f "$file" ] && [ $(wc -c < "$file") -gt 1000000 ]; then
    echo "Error: File too large" >&2
    exit 2
fi

# Check for secrets
if grep -qiE '(password|api[_-]?key)' "$file" 2>/dev/null; then
    echo "Error: Possible secret detected" >&2
    exit 2
fi

exit 0

JSON Output for Advanced Control

Return structured data (advanced):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo '{\"continue\": true, \"decision\": \"allow\"}'"
          }
        ]
      }
    ]
  }
}

Settings File Hierarchy

Load Order (Highest to Lowest Priority)

  1. .claude/settings.local.json - Project-specific, user-only (gitignored)
  2. .claude/settings.json - Project-specific, team-shared (in git)
  3. ~/.claude/settings.json - Global, user-only

Use Cases

Global (~/.claude/settings.json):

  • Personal preferences
  • Universal logging
  • Desktop notifications

Project (.claude/settings.json):

  • Team standards
  • Project-specific automation
  • Shared workflows

Local (.claude/settings.local.json):

  • Personal overrides
  • Development experiments
  • User-specific credentials

Testing Hooks

1. Start Simple

Test with basic echo:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Hook triggered for: $TOOL_ARGS' && exit 0"
          }
        ]
      }
    ]
  }
}

2. Verify Environment Variables

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"Project: $CLAUDE_PROJECT_DIR\" && exit 0"
          }
        ]
      }
    ]
  }
}

3. Test Exit Codes

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'This should block' >&2 && exit 2"
          }
        ]
      }
    ]
  }
}

Try writing a file - should be blocked.

4. Check Timeout

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "sleep 5 && echo 'Done'",
            "timeout": 3
          }
        ]
      }
    ]
  }
}

Should timeout after 3 seconds.

Common Use Cases

Code Quality Enforcement

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write $TOOL_ARGS && npx eslint --fix $TOOL_ARGS && exit 0"
          }
        ]
      }
    ]
  }
}

Security Scanning

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "if grep -qE '(sk-[a-zA-Z0-9]{48}|ghp_[a-zA-Z0-9]{36})' $TOOL_ARGS 2>/dev/null; then echo 'API key detected!' >&2; exit 2; fi; exit 0"
          }
        ]
      }
    ]
  }
}

Automatic Documentation

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "if [[ $TOOL_ARGS == *.js ]]; then npx jsdoc2md $TOOL_ARGS >> API.md; fi; exit 0"
          }
        ]
      }
    ]
  }
}

Change Tracking

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"$(date): $TOOL_NAME $TOOL_ARGS\" >> .claude/audit.log && exit 0"
          }
        ]
      }
    ]
  }
}

Troubleshooting

Hook Not Triggering

Issue: Hook doesn't run when expected

Solutions:

  1. Check matcher pattern matches tool name
  2. Verify settings file location and syntax (valid JSON)
  3. Restart Claude Code to reload settings
  4. Test with simple echo command first

Hook Fails Silently

Issue: Hook runs but doesn't work as expected

Solutions:

  1. Check exit codes (0 for success)
  2. Verify command syntax in terminal first
  3. Check timeout isn't too short
  4. Look at stderr output

Command Not Found

Issue: Hook says command not found

Solutions:

  1. Use full path to command: /usr/local/bin/prettier
  2. Check command is in PATH
  3. Test command in terminal first

Permission Denied

Issue: Hook can't execute script

Solutions:

chmod +x .claude/hooks/script.sh

Best Practices

1. Always Exit with Code

# Good - explicit exit
command && exit 0

# Bad - implicit exit might vary
command

2. Use Timeouts for Long Operations

{
  "type": "command",
  "command": "npm test",
  "timeout": 120  // Don't let tests run forever
}

3. Handle Errors Gracefully

# Check if file exists before processing
if [ -f "$TOOL_ARGS" ]; then
    process_file "$TOOL_ARGS"
else
    echo "File not found" >&2
fi
exit 0  # Don't block on missing file

4. Use Scripts for Complex Logic

Don't put complex bash in JSON:

// Bad
{
  "command": "if [ -f $TOOL_ARGS ]; then cat $TOOL_ARGS | grep pattern | wc -l; fi && exit 0"
}

// Good
{
  "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-file.sh $TOOL_ARGS"
}

5. Test Before Deploying

Test in .claude/settings.local.json before adding to .claude/settings.json.

6. Document Your Hooks

Add comments in settings:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write $TOOL_ARGS",
            "_comment": "Auto-format all JS/TS files on save"
          }
        ]
      }
    ]
  }
}

(Note: JSON doesn't officially support comments, but Claude Code ignores _comment fields)

Security Considerations

Validate Input

# Sanitize TOOL_ARGS before use
if [[ ! $TOOL_ARGS =~ ^[a-zA-Z0-9/_.-]+$ ]]; then
    echo "Invalid file path" >&2
    exit 2
fi

Limit Permissions

Don't use * matcher unless necessary:

// Specific
"matcher": "Write|Edit"

// Too broad
"matcher": "*"

Avoid Destructive Commands

# Dangerous
rm -rf $TOOL_ARGS

# Better - add safety checks
if [[ -f "$TOOL_ARGS" ]] && [[ "$TOOL_ARGS" != "/" ]]; then
    rm "$TOOL_ARGS"
fi

Use Local Settings for Sensitive Operations

Keep potentially dangerous hooks in .claude/settings.local.json (not shared).

Resources


Remember: Hooks are powerful automation tools. Start with simple, safe operations. Test thoroughly before deploying to teams. Use exit codes properly to control flow.