Skip to content
Documentation
Hook

Hooks

What claude-code-mastery runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
claude-code-mastery
5432 skills4 commands5 hooks
Install
$ npx -y skills add TheDecipherist/claude-code-mastery --agent claude-code

Ships with claude-code-mastery. Installing the plugin gets these hooks.

Where it lives

  • hooks/after-edit.shGitHub
    Read the script
    #!/usr/bin/env bash
    # =============================================================================
    # PostToolUse Hook: After File Edit
    # =============================================================================
    #
    # This hook runs AFTER Claude edits or writes a file.
    # Use it for fast operations like formatting that should run immediately.
    #
    # For heavier checks (tests, full linting), use the end-of-turn (Stop) hook.
    #
    # Usage:
    #   Add to ~/.claude/settings.json:
    #   {
    #     "hooks": {
    #       "PostToolUse": [
    #         {
    #           "matcher": "Edit|Write",
    #           "hooks": [
    #             {
    #               "type": "command",
    #               "command": "~/.claude/hooks/after-edit.sh"
    #             }
    #           ]
    #         }
    #       ]
    #     }
    #   }
    # =============================================================================
    
    set -euo pipefail
    
    # Read JSON input from stdin
    INPUT=$(cat)
    
    # Extract the file path
    FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')
    
    if [[ -z "$FILE_PATH" ]]; then
        exit 0  # No file path, nothing to do
    fi
    
    # Get file extension
    EXTENSION="${FILE_PATH##*.}"
    
    # -----------------------------------------------------------------------------
    # Format based on file type
    # -----------------------------------------------------------------------------
    
    case "$EXTENSION" in
        js|jsx|ts|tsx|json|md|yaml|yml|css|scss|html)
            # Prettier for web files
            if command -v prettier &>/dev/null; then
                prettier --write "$FILE_PATH" 2>/dev/null || true
            fi
            ;;
        
        py)
            # Black for Python
            if command -v black &>/dev/null; then
                black --quiet "$FILE_PATH" 2>/dev/null || true
            fi
            # Ruff for linting
            if command -v ruff &>/dev/null; then
                ruff check --fix --silent "$FILE_PATH" 2>/dev/null || true
            fi
            ;;
        
        go)
            # gofmt for Go
            if command -v gofmt &>/dev/null; then
                gofmt -w "$FILE_PATH" 2>/dev/null || true
            fi
            ;;
        
        rs)
            # rustfmt for Rust
            if command -v rustfmt &>/dev/null; then
                rustfmt "$FILE_PATH" 2>/dev/null || true
            fi
            ;;
        
        sh|bash)
            # shfmt for shell scripts
            if command -v shfmt &>/dev/null; then
                shfmt -w "$FILE_PATH" 2>/dev/null || true
            fi
            ;;
    esac
    
    # Always exit 0 - formatting failures shouldn't block work
    exit 0
    
  • hooks/block-dangerous-commands.shGitHub
    Read the script
    #!/usr/bin/env bash
    # =============================================================================
    # PreToolUse Hook: Block Dangerous Bash Commands
    # =============================================================================
    #
    # This hook runs BEFORE bash commands execute.
    # It blocks destructive patterns like rm -rf, force pushes, etc.
    #
    # Exit codes:
    #   0 = Allow command
    #   2 = Block command (stderr fed back to Claude)
    #
    # Usage:
    #   Add to ~/.claude/settings.json:
    #   {
    #     "hooks": {
    #       "PreToolUse": [
    #         {
    #           "matcher": "Bash",
    #           "hooks": [
    #             {
    #               "type": "command",
    #               "command": "~/.claude/hooks/block-dangerous-commands.sh"
    #             }
    #           ]
    #         }
    #       ]
    #     }
    #   }
    # =============================================================================
    
    set -euo pipefail
    
    # Read JSON input from stdin
    INPUT=$(cat)
    
    # Extract the command using jq
    COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
    
    if [[ -z "$COMMAND" ]]; then
        exit 0  # No command, allow
    fi
    
    # -----------------------------------------------------------------------------
    # Dangerous Patterns
    # -----------------------------------------------------------------------------
    
    # rm -rf with dangerous paths
    if echo "$COMMAND" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|--recursive\s+--force|-rf|-fr)\s+(/|~|\.\.|\$HOME|\$\{HOME\})'; then
        echo "๐Ÿ›‘ BLOCKED: Destructive rm command targeting root, home, or parent directory" >&2
        echo "Command: $COMMAND" >&2
        exit 2
    fi
    
    # rm -rf /* or rm -rf ~/*
    if echo "$COMMAND" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|--recursive\s+--force|-rf|-fr)\s+(/\*|~/\*|/home)'; then
        echo "๐Ÿ›‘ BLOCKED: Destructive rm command with wildcard on sensitive path" >&2
        echo "Command: $COMMAND" >&2
        exit 2
    fi
    
    # Force push to main/master
    if echo "$COMMAND" | grep -qE 'git\s+push\s+.*(-f|--force)\s+.*(main|master|production|release)'; then
        echo "๐Ÿ›‘ BLOCKED: Force push to protected branch" >&2
        echo "Command: $COMMAND" >&2
        echo "Tip: Create a PR instead of force pushing to main/master" >&2
        exit 2
    fi
    
    # chmod 777 (world-writable)
    if echo "$COMMAND" | grep -qE 'chmod\s+(777|a\+rwx)'; then
        echo "โš ๏ธ BLOCKED: Setting world-writable permissions (777)" >&2
        echo "Command: $COMMAND" >&2
        echo "Tip: Use 755 for directories, 644 for files" >&2
        exit 2
    fi
    
    # Piping curl directly to shell (dangerous pattern)
    if echo "$COMMAND" | grep -qE 'curl\s+.*\|\s*(ba)?sh'; then
        echo "โš ๏ธ BLOCKED: Piping curl output directly to shell" >&2
        echo "Command: $COMMAND" >&2
        echo "Tip: Download script first, review it, then execute" >&2
        exit 2
    fi
    
    # wget piped to shell
    if echo "$COMMAND" | grep -qE 'wget\s+.*\|\s*(ba)?sh'; then
        echo "โš ๏ธ BLOCKED: Piping wget output directly to shell" >&2
        echo "Command: $COMMAND" >&2
        exit 2
    fi
    
    # dd writing to disk devices
    if echo "$COMMAND" | grep -qE 'dd\s+.*of=/dev/(sd|hd|nvme|disk)'; then
        echo "๐Ÿ›‘ BLOCKED: dd command writing directly to disk device" >&2
        echo "Command: $COMMAND" >&2
        exit 2
    fi
    
    # mkfs (format disk)
    if echo "$COMMAND" | grep -qE 'mkfs'; then
        echo "๐Ÿ›‘ BLOCKED: mkfs command (disk formatting)" >&2
        echo "Command: $COMMAND" >&2
        exit 2
    fi
    
    # Commands that could exfiltrate data
    if echo "$COMMAND" | grep -qE '(curl|wget|nc|netcat)\s+.*\.(env|pem|key|secret)'; then
        echo "โš ๏ธ BLOCKED: Command appears to exfiltrate sensitive files" >&2
        echo "Command: $COMMAND" >&2
        exit 2
    fi
    
    # Reading .env files via cat/less/head/tail
    if echo "$COMMAND" | grep -qE '(cat|less|head|tail|more|bat)\s+.*\.env'; then
        echo "โš ๏ธ BLOCKED: Reading .env file via $COMMAND" >&2
        echo "Tip: Use environment variables instead of reading .env directly" >&2
        exit 2
    fi
    
    # -----------------------------------------------------------------------------
    # Command is safe, allow it
    # -----------------------------------------------------------------------------
    exit 0
    
  • hooks/block-secrets.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """
    PreToolUse hook to block access to sensitive files.
    
    This hook runs BEFORE Claude can read, edit, or write files.
    Exit code 2 blocks the operation and feeds stderr back to Claude.
    
    Usage:
      Add to ~/.claude/settings.json:
      {
        "hooks": {
          "PreToolUse": [
            {
              "matcher": "Read|Edit|Write",
              "hooks": [
                {
                  "type": "command",
                  "command": "python3 ~/.claude/hooks/block-secrets.py"
                }
              ]
            }
          ]
        }
      }
    
    Why this matters:
      - CLAUDE.md rules are suggestions that Claude can override
      - This hook is deterministic enforcement - it ALWAYS runs
      - Even if Claude is "convinced" to read secrets, this blocks it
    """
    import json
    import sys
    from pathlib import Path
    
    # =============================================================================
    # CONFIGURATION - Customize these patterns for your environment
    # =============================================================================
    
    # Exact filenames to block
    SENSITIVE_FILENAMES = {
        # Environment files
        '.env',
        '.env.local',
        '.env.development',
        '.env.development.local',
        '.env.test',
        '.env.test.local',
        '.env.production',
        '.env.production.local',
        '.env.staging',
        
        # Secrets files
        'secrets.json',
        'secrets.yaml',
        'secrets.yml',
        'secrets.toml',
        '.secrets',
        
        # Credentials
        'credentials.json',
        'credentials.yaml',
        'service-account.json',
        'service_account.json',
        
        # SSH keys
        'id_rsa',
        'id_rsa.pub',
        'id_ed25519',
        'id_ed25519.pub',
        'id_ecdsa',
        'id_dsa',
        'known_hosts',
        'authorized_keys',
        
        # Package manager auth
        '.npmrc',
        '.pypirc',
        '.yarnrc',
        '.docker/config.json',
        
        # Cloud credentials
        '.aws/credentials',
        '.aws/config',
        'gcloud/credentials.db',
        '.azure/credentials',
        
        # Git credentials
        '.git-credentials',
        '.gitconfig',  # Can contain credentials
        '.git/config', # Can contain tokens
        
        # Database
        '.pgpass',
        '.my.cnf',
        '.mongorc.js',
    }
    
    # File extensions to block
    SENSITIVE_EXTENSIONS = {
        '.pem',      # Certificates/keys
        '.key',      # Private keys
        '.p12',      # PKCS#12 certificates
        '.pfx',      # Windows certificates
        '.jks',      # Java keystore
        '.keystore', # Generic keystore
        '.crt',      # Certificates (sometimes contain keys)
        '.cer',      # Certificates
    }
    
    # Patterns to match anywhere in path
    SENSITIVE_PATH_PATTERNS = [
        'secret',
        'credential',
        'private_key',
        'privatekey',
        '.env.',     # Catches .env.anything
        '/secrets/', # Secrets directories
    ]
    
    # =============================================================================
    # HOOK LOGIC
    # =============================================================================
    
    def is_sensitive_file(file_path: str) -> tuple[bool, str]:
        """
        Check if a file path matches sensitive patterns.
        Returns (is_sensitive, reason).
        """
        if not file_path:
            return False, ""
        
        path = Path(file_path)
        file_name = path.name
        file_lower = file_path.lower()
        
        # Check exact filename match
        if file_name in SENSITIVE_FILENAMES:
            return True, f"'{file_name}' is a known sensitive file"
        
        # Check extension
        if path.suffix.lower() in SENSITIVE_EXTENSIONS:
            return True, f"'{path.suffix}' files may contain private keys or certificates"
        
        # Check path patterns
        for pattern in SENSITIVE_PATH_PATTERNS:
            if pattern in file_lower:
                return True, f"path contains sensitive pattern '{pattern}'"
        
        return False, ""
    
    
    def extract_file_path(data: dict) -> str:
        """
        Extract file path from tool input.
        Different tools use different parameter names.
        """
        tool_input = data.get('tool_input', {})
        
        # Try common parameter names
        for key in ['file_path', 'path', 'filename', 'file']:
            if key in tool_input:
                return tool_input[key]
        
        # For Bash tool, check the command for file references
        command = tool_input.get('command', '')
        if command:
            # This is a simplified check - you might want more sophisticated parsing
            for pattern in SENSITIVE_FILENAMES:
                if pattern in command:
                    return pattern
            for ext in SENSITIVE_EXTENSIONS:
                if ext in command:
                    return command  # Return command as "file" so it gets blocked
        
        return ""
    
    
    def main():
        try:
            # Read JSON input from stdin
            data = json.load(sys.stdin)
            
            # Get tool name for better error messages
            tool_name = data.get('tool_name', 'unknown')
            
            # Extract file path
            file_path = extract_file_path(data)
            
            if not file_path:
                # No file path found, allow the operation
                sys.exit(0)
            
            # Check if sensitive
            is_sensitive, reason = is_sensitive_file(file_path)
            
            if is_sensitive:
                # Construct error message that will be fed back to Claude
                error_msg = f"""
    โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
    โ•‘                    ๐Ÿ”’ SECURITY HOOK BLOCKED                       โ•‘
    โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
    โ•‘ Tool: {tool_name}
    โ•‘ File: {file_path}
    โ•‘ 
    โ•‘ Reason: {reason}
    โ•‘
    โ•‘ This file likely contains secrets, credentials, or private keys
    โ•‘ that should not be accessed programmatically.
    โ•‘
    โ•‘ Recommended actions:
    โ•‘ โ€ข Use environment variables instead of reading .env directly
    โ•‘ โ€ข Ask the user for specific (non-sensitive) information
    โ•‘ โ€ข Reference .env.example for variable names only
    โ•‘ โ€ข Store secrets in a proper secrets manager
    โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
    """.strip()
                
                # Print to stderr (will be fed back to Claude)
                print(error_msg, file=sys.s
  • hooks/end-of-turn.shGitHub
    Read the script
    #!/usr/bin/env bash
    # =============================================================================
    # End-of-Turn Quality Gate Hook
    # =============================================================================
    #
    # This hook runs when Claude finishes responding (Stop event).
    # It performs quality checks to catch issues before they accumulate.
    #
    # Usage:
    #   Add to ~/.claude/settings.json or .claude/settings.json:
    #   {
    #     "hooks": {
    #       "Stop": [
    #         {
    #           "matcher": "*",
    #           "hooks": [
    #             {
    #               "type": "command",
    #               "command": "~/.claude/hooks/end-of-turn.sh"
    #             }
    #           ]
    #         }
    #       ]
    #     }
    #   }
    #
    # Exit codes:
    #   0 = Success (continue normally)
    #   1 = Error shown to user
    #   2 = Block and feed stderr to Claude (use sparingly for Stop hooks)
    #
    # =============================================================================
    
    set -euo pipefail
    
    # -----------------------------------------------------------------------------
    # Configuration
    # -----------------------------------------------------------------------------
    
    # Set to "true" to enable verbose logging
    VERBOSE="${CLAUDE_HOOK_VERBOSE:-false}"
    
    # Maximum time for checks (seconds)
    TIMEOUT=30
    
    # -----------------------------------------------------------------------------
    # Helper Functions
    # -----------------------------------------------------------------------------
    
    log() {
        if [[ "$VERBOSE" == "true" ]]; then
            echo "[end-of-turn] $*" >&2
        fi
    }
    
    run_check() {
        local name="$1"
        local cmd="$2"
        
        log "Running: $name"
        
        if timeout "$TIMEOUT" bash -c "$cmd" 2>/dev/null; then
            log "โœ“ $name passed"
            return 0
        else
            log "โœ— $name failed (non-blocking)"
            return 0  # Don't fail the hook, just log
        fi
    }
    
    # -----------------------------------------------------------------------------
    # Detect Project Type
    # -----------------------------------------------------------------------------
    
    is_nodejs() {
        [[ -f "package.json" ]]
    }
    
    is_typescript() {
        [[ -f "tsconfig.json" ]]
    }
    
    is_python() {
        [[ -f "pyproject.toml" ]] || [[ -f "setup.py" ]] || [[ -f "requirements.txt" ]]
    }
    
    is_rust() {
        [[ -f "Cargo.toml" ]]
    }
    
    is_go() {
        [[ -f "go.mod" ]]
    }
    
    # -----------------------------------------------------------------------------
    # Project-Specific Checks
    # -----------------------------------------------------------------------------
    
    check_nodejs() {
        log "Detected Node.js project"
        
        # Check if node_modules exists
        if [[ ! -d "node_modules" ]]; then
            log "node_modules missing, skipping npm checks"
            return 0
        fi
        
        # Run lint if available
        if grep -q '"lint"' package.json 2>/dev/null; then
            run_check "npm lint" "npm run lint --silent"
        fi
        
        # Run typecheck if TypeScript
        if is_typescript; then
            if grep -q '"typecheck"' package.json 2>/dev/null; then
                run_check "typecheck" "npm run typecheck --silent"
            elif command -v tsc &>/dev/null; then
                run_check "tsc" "tsc --noEmit"
            fi
        fi
    }
    
    check_python() {
        log "Detected Python project"
        
        # Ruff (fast Python linter)
        if command -v ruff &>/dev/null; then
            run_check "ruff" "ruff check . --fix --silent"
        fi
        
        # Black (formatter)
        if command -v black &>/dev/null; then
            run_check "black" "black --check --quiet ."
        fi
        
        # MyPy (type checker)
        if command -v mypy &>/dev/null && [[ -f "mypy.ini" || -f "pyproject.toml" ]]; then
            run_check "mypy" "mypy . --silent-imports"
        fi
    }
    
    check_rust() {
        log "Detected Rust project"
        
        # Cargo check (fast type checking)
        if command -v cargo &>/dev/null; then
            run_check "cargo check" "cargo check --quiet"
        fi
        
        # Clippy (linter)
        if command -v cargo &>/dev/null; then
            run_check "clippy" "cargo clippy --quiet -- -D warnings"
        fi
    }
    
    check_go() {
        log "Detected Go project"
        
        # Go vet
        if command -v go &>/dev/null; then
            run_check "go vet" "go vet ./..."
        fi
        
        # Staticcheck
        if command -v staticcheck &>/dev/null; then
            run_check "staticcheck" "staticcheck ./..."
        fi
    }
    
    # -----------------------------------------------------------------------------
    # Universal Checks
    # -----------------------------------------------------------------------------
    
    check_secrets() {
        log "Checking for exposed secrets"
        
        # Simple grep for common secret patterns in staged files
        if git rev-parse --git-dir &>/dev/null; then
            local staged_files
            staged_files=$(git diff --cached --name-only 2>/dev/null || true)
            
            if [[ -n "$staged_files" ]]; then
                # Check for hardcoded secrets (simplified pattern)
                if echo "$staged_files" | xargs grep -l -E "(API_KEY|SECRET|TOKEN|PASSWORD)\s*[=:]\s*['\"][A-Za-z0-9_\-]{16,}" 2>/dev/null; then
                    echo "โš ๏ธ  Warning: Possible hardcoded secrets in staged files" >&2
                fi
            fi
        fi
    }
    
    check_env_committed() {
        log "Checking .env not staged"
        
        if git rev-parse --git-dir &>/dev/null; then
            if git diff --cached --name-only 2>/dev/null | grep -q "^\.env"; then
                echo "โš ๏ธ  Warning: .env file is staged for commit!" >&2
            fi
        fi
    }
    
    # -----------------------------------------------------------------------------
    # Main
    # -----------------------------------------------------------------------------
    
    main() {
        log "Starting end-of-turn checks"
        
        # Run project-specific checks
        if is_nodejs; then
            check_nodejs
        fi
        
        if is_python; then
            check_python
        fi
        
        if is_rust; then
            check_rust
        fi
        
        if is_go; then
            check_go
        fi
        
        # Universal checks
        check_secrets
        check_env_committed
        
        log "End-of-turn checks complete"
        exit 0
    }
    
    main "$@"
    
  • hooks/notify.shGitHub
    Read the script
    #!/usr/bin/env bash
    # =============================================================================
    # Notification Hook: Desktop Alerts
    # =============================================================================
    #
    # This hook runs when Claude Code sends notifications.
    # It triggers desktop notifications so you know when Claude needs input.
    #
    # Works on:
    #   - macOS (osascript)
    #   - Linux (notify-send)
    #   - Windows WSL (powershell)
    #
    # Usage:
    #   Add to ~/.claude/settings.json:
    #   {
    #     "hooks": {
    #       "Notification": [
    #         {
    #           "matcher": "*",
    #           "hooks": [
    #             {
    #               "type": "command",
    #               "command": "~/.claude/hooks/notify.sh"
    #             }
    #           ]
    #         }
    #       ]
    #     }
    #   }
    # =============================================================================
    
    set -euo pipefail
    
    # Read JSON input from stdin
    INPUT=$(cat)
    
    # Extract notification content
    CONTENT=$(echo "$INPUT" | jq -r '.content // "Claude needs your attention"')
    
    # Truncate long messages
    if [[ ${#CONTENT} -gt 100 ]]; then
        CONTENT="${CONTENT:0:100}..."
    fi
    
    # -----------------------------------------------------------------------------
    # Send notification based on OS
    # -----------------------------------------------------------------------------
    
    send_notification() {
        local title="Claude Code"
        local message="$1"
        
        # macOS
        if [[ "$OSTYPE" == "darwin"* ]]; then
            osascript -e "display notification \"$message\" with title \"$title\" sound name \"Glass\"" 2>/dev/null || true
            return
        fi
        
        # Linux with notify-send
        if command -v notify-send &>/dev/null; then
            notify-send "$title" "$message" -u normal -t 5000 2>/dev/null || true
            return
        fi
        
        # Windows WSL
        if grep -qi microsoft /proc/version 2>/dev/null; then
            powershell.exe -Command "
                [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
                [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
                \$template = '<toast><visual><binding template=\"ToastText02\"><text id=\"1\">$title</text><text id=\"2\">$message</text></binding></visual></toast>'
                \$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
                \$xml.LoadXml(\$template)
                \$toast = [Windows.UI.Notifications.ToastNotification]::new(\$xml)
                [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Claude Code').Show(\$toast)
            " 2>/dev/null || true
            return
        fi
        
        # Fallback: terminal bell
        echo -e '\a'
    }
    
    send_notification "$CONTENT"
    exit 0
    

Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.

Ships withclaude-code-mastery

The complete guide to maximizing Claude Code: Global CLAUDE.md, MCP Servers, Commands, Hooks, Skills, and Why Single-Purpose Chats Matter. This version is obsolete by now.

Get the whole plugin
Stats
543
Stars
46
Forks
Maintained
Maintenance
Shell
Language
MIT
License
3mo ago
Last commit
6mo ago
Created

Repo: TheDecipherist/claude-code-mastery