Files
gh-withzombies-hyperpowers/skills/building-hooks/resources/hook-patterns.md
2025-11-30 09:06:38 +08:00

11 KiB

Hook Patterns Library

Reusable patterns for common hook use cases.

Pattern: File Path Validation

Safely validate and sanitize file paths in hooks.

validate_file_path() {
    local path="$1"

    # Remove null/empty
    if [ -z "$path" ] || [ "$path" == "null" ]; then
        return 1
    fi

    # Must be absolute path
    if [[ ! "$path" =~ ^/ ]]; then
        return 1
    fi

    # Must exist
    if [ ! -f "$path" ]; then
        return 1
    fi

    # Check file extension whitelist
    if [[ ! "$path" =~ \.(ts|tsx|js|jsx|py|rs|go|java)$ ]]; then
        return 1
    fi

    return 0
}

# Usage
if validate_file_path "$file_path"; then
    # Safe to operate on file
    process_file "$file_path"
fi

Pattern: Finding Project Root

Locate the project root directory from any file path.

find_project_root() {
    local dir="$1"

    # Start from file's directory
    if [ -f "$dir" ]; then
        dir=$(dirname "$dir")
    fi

    # Walk up until finding markers
    while [ "$dir" != "/" ]; do
        # Check for project markers
        if [ -f "$dir/package.json" ] || \
           [ -f "$dir/Cargo.toml" ] || \
           [ -f "$dir/go.mod" ] || \
           [ -d "$dir/.git" ]; then
            echo "$dir"
            return 0
        fi
        dir=$(dirname "$dir")
    done

    return 1
}

# Usage
project_root=$(find_project_root "$file_path")
if [ -n "$project_root" ]; then
    cd "$project_root"
    npm run build
fi

Pattern: Conditional Hook Execution

Run hook only when certain conditions are met.

#!/bin/bash

# Configuration
MIN_CHANGES=3
TARGET_REPO="backend"

# Check if should run
should_run() {
    # Count recent edits
    local edit_count=$(tail -20 ~/.claude/edit-log.txt | wc -l)

    if [ "$edit_count" -lt "$MIN_CHANGES" ]; then
        return 1
    fi

    # Check if target repo was modified
    if ! tail -20 ~/.claude/edit-log.txt | grep -q "$TARGET_REPO"; then
        return 1
    fi

    return 0
}

# Main execution
if ! should_run; then
    echo '{}'
    exit 0
fi

# Run actual hook logic
perform_build_check

Pattern: Rate Limiting

Prevent hooks from running too frequently.

#!/bin/bash

RATE_LIMIT_FILE="/tmp/hook-last-run"
MIN_INTERVAL=30  # seconds

# Check if enough time has passed
should_run() {
    if [ ! -f "$RATE_LIMIT_FILE" ]; then
        return 0
    fi

    local last_run=$(cat "$RATE_LIMIT_FILE")
    local now=$(date +%s)
    local elapsed=$((now - last_run))

    if [ "$elapsed" -lt "$MIN_INTERVAL" ]; then
        echo "Skipping (ran ${elapsed}s ago, min interval ${MIN_INTERVAL}s)"
        return 1
    fi

    return 0
}

# Update last run time
mark_run() {
    date +%s > "$RATE_LIMIT_FILE"
}

# Usage
if should_run; then
    perform_expensive_operation
    mark_run
fi

echo '{}'

Pattern: Multi-Project Detection

Detect which project/repo a file belongs to.

detect_project() {
    local file="$1"
    local project_root="/Users/myuser/projects"

    # Extract project name from path
    if [[ "$file" =~ $project_root/([^/]+) ]]; then
        echo "${BASH_REMATCH[1]}"
        return 0
    fi

    echo "unknown"
    return 1
}

# Usage
project=$(detect_project "$file_path")

case "$project" in
    "frontend")
        npm --prefix ~/projects/frontend run build
        ;;
    "backend")
        cargo build --manifest-path ~/projects/backend/Cargo.toml
        ;;
    *)
        echo "Unknown project: $project"
        ;;
esac

Pattern: Graceful Degradation

Handle failures gracefully without blocking workflow.

#!/bin/bash

# Try operation with fallback
try_with_fallback() {
    local primary_cmd="$1"
    local fallback_cmd="$2"
    local description="$3"

    echo "Attempting: $description"

    # Try primary command
    if eval "$primary_cmd" 2>/dev/null; then
        echo "✅ Success"
        return 0
    fi

    echo "⚠️  Primary failed, trying fallback..."

    # Try fallback
    if eval "$fallback_cmd" 2>/dev/null; then
        echo "✅ Fallback succeeded"
        return 0
    fi

    echo "❌ Both failed, continuing anyway"
    return 1
}

# Usage
try_with_fallback \
    "npm run build" \
    "npm run build:dev" \
    "Building project"

# Always return empty response (non-blocking)
echo '{}'

Pattern: Parallel Execution

Run multiple checks in parallel for speed.

#!/bin/bash

# Run checks in parallel
run_parallel_checks() {
    local pids=()

    # Start each check in background
    check_typescript &
    pids+=($!)

    check_eslint &
    pids+=($!)

    check_tests &
    pids+=($!)

    # Wait for all to complete
    local exit_code=0
    for pid in "${pids[@]}"; do
        wait "$pid" || exit_code=1
    done

    return $exit_code
}

check_typescript() {
    npx tsc --noEmit > /tmp/tsc-output.txt 2>&1
    if [ $? -ne 0 ]; then
        echo "TypeScript errors found"
        return 1
    fi
}

check_eslint() {
    npx eslint . > /tmp/eslint-output.txt 2>&1
}

check_tests() {
    npm test > /tmp/test-output.txt 2>&1
}

# Usage
if run_parallel_checks; then
    echo "✅ All checks passed"
else
    echo "⚠️  Some checks failed"
    cat /tmp/tsc-output.txt
    cat /tmp/eslint-output.txt
fi

echo '{}'

Pattern: Smart Caching

Cache results to avoid redundant work.

#!/bin/bash

CACHE_DIR="$HOME/.claude/hook-cache"
mkdir -p "$CACHE_DIR"

# Generate cache key
cache_key() {
    local file="$1"
    echo -n "$file:$(stat -f %m "$file" 2>/dev/null || stat -c %Y "$file")" | md5sum | cut -d' ' -f1
}

# Check cache
check_cache() {
    local file="$1"
    local key=$(cache_key "$file")
    local cache_file="$CACHE_DIR/$key"

    if [ -f "$cache_file" ]; then
        # Cache hit
        cat "$cache_file"
        return 0
    fi

    return 1
}

# Update cache
update_cache() {
    local file="$1"
    local result="$2"
    local key=$(cache_key "$file")
    local cache_file="$CACHE_DIR/$key"

    echo "$result" > "$cache_file"

    # Clean old cache entries (older than 1 day)
    find "$CACHE_DIR" -type f -mtime +1 -delete 2>/dev/null
}

# Usage
if cached=$(check_cache "$file_path"); then
    echo "Cache hit: $cached"
else
    result=$(expensive_operation "$file_path")
    update_cache "$file_path" "$result"
    echo "Computed: $result"
fi

Pattern: Progressive Output

Show progress for long-running hooks.

#!/bin/bash

# Progress indicator
show_progress() {
    local message="$1"
    echo -n "$message..."
}

complete_progress() {
    local status="$1"
    if [ "$status" == "success" ]; then
        echo " ✅"
    else
        echo " ❌"
    fi
}

# Usage
show_progress "Running TypeScript compiler"
if npx tsc --noEmit 2>/dev/null; then
    complete_progress "success"
else
    complete_progress "failure"
fi

show_progress "Running linter"
if npx eslint . 2>/dev/null; then
    complete_progress "success"
else
    complete_progress "failure"
fi

echo '{}'

Pattern: Context Injection

Inject helpful context into Claude's prompt.

// UserPromptSubmit hook
function injectContext(prompt) {
    const context = [];

    // Add relevant documentation
    if (prompt.includes('API')) {
        context.push('📖 API Documentation: https://docs.example.com/api');
    }

    // Add recent changes
    const recentFiles = getRecentlyEditedFiles();
    if (recentFiles.length > 0) {
        context.push(`📝 Recently edited: ${recentFiles.join(', ')}`);
    }

    // Add project status
    const buildStatus = getLastBuildStatus();
    if (!buildStatus.passed) {
        context.push(`⚠️  Current build has ${buildStatus.errorCount} errors`);
    }

    if (context.length === 0) {
        return { decision: 'approve' };
    }

    return {
        decision: 'approve',
        additionalContext: `\n\n---\n${context.join('\n')}\n---\n`
    };
}

Pattern: Error Accumulation

Collect multiple errors before reporting.

#!/bin/bash

ERRORS=()

# Add error to collection
add_error() {
    ERRORS+=("$1")
}

# Report all errors
report_errors() {
    if [ ${#ERRORS[@]} -eq 0 ]; then
        echo "✅ No errors found"
        return 0
    fi

    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo "⚠️  Found ${#ERRORS[@]} issue(s):"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

    local i=1
    for error in "${ERRORS[@]}"; do
        echo "$i. $error"
        ((i++))
    done

    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    return 1
}

# Usage
if ! run_typescript_check; then
    add_error "TypeScript compilation failed"
fi

if ! run_lint_check; then
    add_error "Linting issues found"
fi

if ! run_test_check; then
    add_error "Tests failing"
fi

report_errors

echo '{}'

Pattern: Conditional Blocking

Block only on critical errors, warn on others.

#!/bin/bash

ERROR_LEVEL="none"  # none, warning, critical

# Check for issues
check_critical_issues() {
    if grep -q "FIXME\|XXX\|TODO: CRITICAL" "$file_path"; then
        ERROR_LEVEL="critical"
        return 1
    fi
    return 0
}

check_warnings() {
    if grep -q "console.log\|debugger" "$file_path"; then
        ERROR_LEVEL="warning"
        return 1
    fi
    return 0
}

# Run checks
check_critical_issues
check_warnings

# Return appropriate decision
case "$ERROR_LEVEL" in
    "critical")
        echo '{
            "decision": "block",
            "reason": "🚫 CRITICAL: Found critical TODOs or FIXMEs that must be addressed"
        }' | jq -c '.'
        ;;
    "warning")
        echo "⚠️  Warning: Found debug statements (console.log, debugger)"
        echo '{}'
        ;;
    *)
        echo '{}'
        ;;
esac

Pattern: Hook Coordination

Coordinate between multiple hooks using shared state.

# Hook 1: Track state
#!/bin/bash
STATE_FILE="/tmp/hook-state.json"

# Update state
jq -n \
    --arg timestamp "$(date +%s)" \
    --arg files "$files_edited" \
    '{lastRun: $timestamp, filesEdited: ($files | split(","))}' \
    > "$STATE_FILE"

echo '{}'
# Hook 2: Read state
#!/bin/bash
STATE_FILE="/tmp/hook-state.json"

if [ -f "$STATE_FILE" ]; then
    last_run=$(jq -r '.lastRun' "$STATE_FILE")
    files=$(jq -r '.filesEdited[]' "$STATE_FILE")

    # Use state from previous hook
    for file in $files; do
        process_file "$file"
    done
fi

echo '{}'

Pattern: User Notification

Notify user of important events without blocking.

#!/bin/bash

# Send desktop notification (macOS)
notify_macos() {
    osascript -e "display notification \"$1\" with title \"Claude Code Hook\""
}

# Send desktop notification (Linux)
notify_linux() {
    notify-send "Claude Code Hook" "$1"
}

# Notify based on OS
notify() {
    local message="$1"

    case "$OSTYPE" in
        darwin*)
            notify_macos "$message"
            ;;
        linux*)
            notify_linux "$message"
            ;;
    esac
}

# Usage
if [ "$error_count" -gt 10 ]; then
    notify "⚠️  Build has $error_count errors"
fi

echo '{}'

Remember

  • Keep it simple - Start with basic patterns, add complexity only when needed
  • Test thoroughly - Test each pattern in isolation before combining
  • Fail gracefully - Non-blocking hooks should never crash workflow
  • Log everything - You'll need it for debugging
  • Document patterns - Future you will thank present you