Skip to content
Skill Authoring
Hook

Hooks

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

From plugin
jarrodwatts-claude-code-config
1.1k5 skills4 agents1 command4 hooks
Install
$ npx -y skills add jarrodwatts/claude-code-config --agent claude-code

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

Where it lives

  • hooks/check-comments.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """
    Comment Checker Hook for Claude Code
    Analyzes code changes for excessive comments
    """
    
    import json
    import sys
    import re
    from pathlib import Path
    
    # Configuration
    MAX_COMMENT_RATIO = 0.25  # 25% comments triggers warning
    CODE_EXTENSIONS = {'.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.java', '.cpp', '.c', '.sol'}
    
    # Patterns for valid comments (should not be flagged)
    VALID_PATTERNS = [
        r'^\s*#\s*#?\s*(given|when|then|and|but)\b',  # BDD comments (Python)
        r'^\s*//\s*#?\s*(given|when|then|and|but)\b',  # BDD comments (JS/TS)
        r'^\s*"""',  # Python docstrings
        r"^\s*'''",  # Python docstrings
        r'^\s*/\*\*',  # JSDoc/JavaDoc
        r'^\s*\*\s*@',  # JSDoc annotations
        r'^\s*#!',  # Shebang
        r'^\s*//\s*@ts-',  # TypeScript directives
        r'^\s*//\s*eslint-',  # ESLint directives
        r'^\s*#\s*type:',  # Python type comments
        r'^\s*#\s*noqa',  # Python noqa
        r'^\s*#\s*pragma',  # Pragma directives
        r'^\s*//\s*TODO:',  # TODO comments (acceptable)
        r'^\s*//\s*FIXME:',  # FIXME comments (acceptable)
        r'^\s*#\s*TODO:',  # TODO comments
        r'^\s*#\s*FIXME:',  # FIXME comments
        r'^\s*///\s*',  # Rust doc comments
        r'^\s*///',  # Solidity NatSpec
        r'^\s*@dev',  # Solidity dev comments
        r'^\s*@param',  # Parameter docs
        r'^\s*@return',  # Return docs
        r'^\s*@notice',  # Solidity notice
    ]
    
    def is_valid_comment(line: str) -> bool:
        """Check if a comment line is a valid/acceptable pattern"""
        for pattern in VALID_PATTERNS:
            if re.match(pattern, line, re.IGNORECASE):
                return True
        return False
    
    def is_comment_line(line: str, ext: str) -> bool:
        """Check if a line is a comment"""
        stripped = line.strip()
        if not stripped:
            return False
        
        if ext in {'.py'}:
            return stripped.startswith('#')
        elif ext in {'.ts', '.tsx', '.js', '.jsx', '.java', '.go', '.rs', '.cpp', '.c', '.sol'}:
            return stripped.startswith('//') or stripped.startswith('/*') or stripped.startswith('*')
        
        return False
    
    def analyze_content(content: str, file_path: str) -> dict:
        """Analyze code content for comment ratio"""
        ext = Path(file_path).suffix.lower()
        
        if ext not in CODE_EXTENSIONS:
            return {"skip": True, "reason": "Not a code file"}
        
        lines = content.split('\n')
        total_lines = len([l for l in lines if l.strip()])  # Non-empty lines
        
        if total_lines == 0:
            return {"skip": True, "reason": "Empty file"}
        
        comment_lines = []
        flagged_comments = []
        
        for i, line in enumerate(lines, 1):
            if is_comment_line(line, ext):
                comment_lines.append(i)
                if not is_valid_comment(line):
                    flagged_comments.append((i, line.strip()))
        
        comment_ratio = len(comment_lines) / total_lines if total_lines > 0 else 0
        
        return {
            "skip": False,
            "total_lines": total_lines,
            "comment_lines": len(comment_lines),
            "flagged_comments": flagged_comments,
            "comment_ratio": comment_ratio,
            "excessive": comment_ratio > MAX_COMMENT_RATIO and len(flagged_comments) > 0
        }
    
    def main():
        # Debug logging to file
        import os
        debug_log = os.path.expanduser("~/.claude/hooks/debug.log")
    
        try:
            raw_input = sys.stdin.read()
            with open(debug_log, "a") as f:
                f.write(f"\n=== check-comments.py called ===\n")
                f.write(f"Raw input: {raw_input[:2000]}\n")
            input_data = json.loads(raw_input)
        except json.JSONDecodeError as e:
            with open(debug_log, "a") as f:
                f.write(f"JSON decode error: {e}\n")
            sys.exit(0)
        except Exception as e:
            with open(debug_log, "a") as f:
                f.write(f"Error reading stdin: {e}\n")
            sys.exit(0)
    
        with open(debug_log, "a") as f:
            f.write(f"Parsed input keys: {list(input_data.keys())}\n")
    
        tool_name = input_data.get("tool_name", "")
        tool_input = input_data.get("tool_input", {})
        tool_response = input_data.get("tool_response", {})
    
        file_path = tool_input.get("file_path") or tool_input.get("filePath") or tool_input.get("path", "")
    
        # Get content based on tool type
        content = ""
        if tool_name == "Write":
            content = tool_input.get("content", "")
        elif tool_name in ("Edit", "MultiEdit"):
            # For Edit, read the file after the edit was applied
            if file_path and Path(file_path).exists():
                try:
                    content = Path(file_path).read_text()
                except Exception as e:
                    with open(debug_log, "a") as f:
                        f.write(f"Error reading file: {e}\n")
                    sys.exit(0)
    
        with open(debug_log, "a") as f:
            f.write(f"Tool: {tool_name}, File: {file_path}, Content length: {len(content)}\n")
    
        if not file_path or not content:
            sys.exit(0)
        
        result = analyze_content(content, file_path)
    
        with open(debug_log, "a") as f:
            f.write(f"Analysis result: {result}\n")
    
        if result.get("skip"):
            sys.exit(0)
    
        if result.get("excessive"):
            ratio_pct = f"{result['comment_ratio']:.0%}"
            
            # Build warning message
            warning_lines = [
                "",
                "---",
                f"**Comment Check Warning**: {ratio_pct} of lines are comments ({result['comment_lines']}/{result['total_lines']})",
                "",
                "Flagged comments that may be unnecessary:",
            ]
            
            for line_num, comment in result['flagged_comments'][:5]:  # Show max 5
                warning_lines.append(f"  Line {line_num}: `{comment[:60]}{'...' if len(comment) > 60 else ''}`")
            
            if len(result['flagged_comments']) > 5:
                warning_lines.append(f"  ... and {len(result['flagged_comments']) - 5} more")
            
            warning_lines.extend([
                "",
                "**Recommendation**: Code should be self-documenting. Consider:",
                "- Using descriptive variable/fun
  • hooks/keyword-detector.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """
    Keyword Detector Hook for Claude Code
    Detects special keywords and injects mode-specific context
    """
    
    import json
    import sys
    import re
    
    def main():
        try:
            input_data = json.load(sys.stdin)
        except json.JSONDecodeError:
            sys.exit(0)
        
        prompt = input_data.get("prompt", "").lower()
        
        additional_context = None
        
        # Ultrawork mode - maximum performance
        if re.search(r'\b(ultrawork|ulw|ultra\s*work)\b', prompt):
            additional_context = """
    [ULTRAWORK MODE ACTIVATED]
    
    Execute with maximum capability:
    
    1. **Parallel Execution**: Launch multiple agents/tools simultaneously
    2. **Comprehensive Planning**: Create detailed todo list BEFORE starting
    3. **Thorough Verification**: Run diagnostics on all changed files
    4. **No Premature Stopping**: Continue until ALL tasks complete
    5. **Evidence-Based**: Verify each change works correctly
    
    Workflow:
    - Use Task tool to delegate to specialized agents (explore, oracle)
    - Launch independent searches in parallel
    - Create todos for complex multi-step work
    - Mark todos complete only after verification
    """
    
        # Search mode - maximized search effort
        elif re.search(r'\b(search|find|locate|where\s+is)\b', prompt):
            additional_context = """
    [SEARCH MODE ACTIVATED]
    
    Maximize search thoroughness:
    
    1. **Parallel Searches**: Launch multiple search operations simultaneously
    2. **Multiple Angles**: Search by name, content, pattern, and structure
    3. **Cross-Reference**: Verify findings across multiple sources
    4. **Exhaustive**: Don't stop at first result - find ALL matches
    
    Tools to use in parallel:
    - Grep for text patterns
    - Glob for file patterns
    - LSP for symbol definitions/references
    - Git for history when relevant
    
    Report:
    - All matching files with absolute paths
    - Relevance explanation for each match
    - Confidence level in completeness
    """
    
        # Analysis mode - deep investigation
        elif re.search(r'\b(analyze|investigate|debug|diagnose)\b', prompt):
            additional_context = """
    [ANALYSIS MODE ACTIVATED]
    
    Deep investigation protocol:
    
    1. **Gather Evidence**: Read all relevant files before forming conclusions
    2. **Multi-Phase Analysis**:
       - Phase 1: Surface-level scan
       - Phase 2: Deep dive into suspicious areas
       - Phase 3: Cross-reference and validate
    3. **Consult Experts**: Use oracle agent for complex reasoning
    4. **Document Findings**: Systematic, evidence-based conclusions
    
    For debugging:
    - Check recent changes (git log, git blame)
    - Trace data flow through the system
    - Identify edge cases and error paths
    - Propose hypothesis and test it
    """
    
        # Think mode - extended reasoning (note: Claude Code has native ultrathink)
        elif re.search(r'\b(think\s*(deeply|hard|carefully))\b', prompt):
            additional_context = """
    [EXTENDED THINKING MODE]
    
    Take time for thorough reasoning:
    
    1. **Step Back**: Consider the broader context and implications
    2. **Multiple Perspectives**: Evaluate different approaches
    3. **Trade-off Analysis**: Document pros/cons of each option
    4. **Risk Assessment**: Identify potential issues before implementing
    5. **Validation Plan**: How will we verify success?
    
    Before acting:
    - State your understanding of the problem
    - List assumptions being made
    - Outline the approach with rationale
    - Identify potential failure modes
    """
    
        if additional_context:
            output = {
                "hookSpecificOutput": {
                    "hookEventName": "UserPromptSubmit",
                    "additionalContext": additional_context.strip()
                }
            }
            print(json.dumps(output))
        
        sys.exit(0)
    
    if __name__ == "__main__":
        main()
    
    
  • hooks/skill-reminder.shGitHub
    Read the script
    #!/usr/bin/env bash
    set -euo pipefail
    
    jq -n '{"hookSpecificOutput":{"additionalContext":"IMPORTANT: before writing code: invoke any relevant skills using the Skill tool."}}'
    
  • hooks/todo-enforcer.shGitHub
    Read the script
    #!/usr/bin/env bash
    #
    # Todo Enforcer - Claude Code Stop Hook
    # Blocks exit when incomplete todos exist in the session.
    #
    # SPDX-License-Identifier: MIT
    # Copyright (c) 2025
    #
    # Usage: Configure in ~/.claude/settings.json under hooks.Stop
    # Disable: Set {"enabled": false} in ~/.claude/hooks/todo-enforcer.config.json
    
    # shellcheck disable=SC2155
    
    set -euo pipefail
    
    readonly CONFIG_FILE="$HOME/.claude/hooks/todo-enforcer.config.json"
    readonly DEBUG_LOG="$HOME/.claude/hooks/todo-enforcer.log"
    readonly MAX_CONSECUTIVE_BLOCKS=10
    
    log() {
      echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${2:-INFO}] $1" >> "$DEBUG_LOG" 2>/dev/null || true
    }
    
    die() {
      log "$1" "ERROR"
      exit 0
    }
    
    if ! command -v jq &>/dev/null; then
      die "jq is required but not installed"
    fi
    
    # Defer to ralph-wiggum when a loop is active
    [[ -f ".claude/ralph-loop.local.md" ]] && exit 0
    
    load_config() {
      if [[ -f "$CONFIG_FILE" ]]; then
        cat "$CONFIG_FILE" 2>/dev/null || echo '{"enabled":true,"block_count":0}'
      else
        echo '{"enabled":true,"block_count":0}'
      fi
    }
    
    save_config() {
      local temp_file="${CONFIG_FILE}.tmp.$$"
      echo "$1" > "$temp_file" 2>/dev/null && mv "$temp_file" "$CONFIG_FILE" 2>/dev/null || true
    }
    
    allow_exit() {
      [[ -n "${1:-}" ]] && log "Allowing exit: $1"
      exit 0
    }
    
    HOOK_INPUT=$(cat)
    
    log "Hook started"
    
    CONFIG=$(load_config)
    
    if [[ "$(echo "$CONFIG" | jq -r '.enabled // true')" != "true" ]]; then
      allow_exit "Disabled via config"
    fi
    
    read -r SESSION_ID TRANSCRIPT_PATH STOP_HOOK_ACTIVE < <(
      echo "$HOOK_INPUT" | jq -r '[.session_id // "unknown", .transcript_path // "", .stop_hook_active // false] | @tsv'
    )
    
    log "Session: $SESSION_ID | stop_hook_active: $STOP_HOOK_ACTIVE"
    
    if [[ "$STOP_HOOK_ACTIVE" == "true" ]]; then
      if [[ "$(echo "$CONFIG" | jq -r '.last_block_session // ""')" == "$SESSION_ID" ]]; then
        CONFIG=$(echo "$CONFIG" | jq '.block_count = 0')
        save_config "$CONFIG"
      fi
    fi
    
    [[ -z "$TRANSCRIPT_PATH" ]] && allow_exit "No transcript path"
    [[ ! -f "$TRANSCRIPT_PATH" ]] && allow_exit "Transcript not found"
    
    TODOS_JSON=$(jq -s '
      [.[] | .message.content[]? | select(.type == "tool_use" and .name == "TodoWrite") | .input.todos] |
      last // empty
    ' "$TRANSCRIPT_PATH" 2>/dev/null || echo "")
    
    if [[ -z "$TODOS_JSON" || "$TODOS_JSON" == "null" ]]; then
      CONFIG=$(echo "$CONFIG" | jq '.block_count = 0')
      save_config "$CONFIG"
      allow_exit "No todos found"
    fi
    
    read -r PENDING_COUNT IN_PROGRESS_COUNT < <(
      echo "$TODOS_JSON" | jq -r '[
        [.[] | select(.status == "pending")] | length,
        [.[] | select(.status == "in_progress")] | length
      ] | @tsv'
    )
    
    INCOMPLETE_COUNT=$((PENDING_COUNT + IN_PROGRESS_COUNT))
    
    log "Pending: $PENDING_COUNT, In progress: $IN_PROGRESS_COUNT"
    
    if [[ "$INCOMPLETE_COUNT" -eq 0 ]]; then
      CONFIG=$(echo "$CONFIG" | jq '.block_count = 0')
      save_config "$CONFIG"
      allow_exit "All todos completed"
    fi
    
    read -r BLOCK_COUNT LAST_SESSION < <(
      echo "$CONFIG" | jq -r '[.block_count // 0, .last_block_session // ""] | @tsv'
    )
    
    if [[ "$LAST_SESSION" == "$SESSION_ID" ]]; then
      BLOCK_COUNT=$((BLOCK_COUNT + 1))
    else
      BLOCK_COUNT=1
    fi
    
    if [[ "$BLOCK_COUNT" -ge "$MAX_CONSECUTIVE_BLOCKS" ]]; then
      log "Safety valve triggered after $MAX_CONSECUTIVE_BLOCKS blocks" "WARN"
      CONFIG=$(echo "$CONFIG" | jq '.block_count = 0')
      save_config "$CONFIG"
      allow_exit "Safety valve triggered"
    fi
    
    CONFIG=$(echo "$CONFIG" | jq --arg sid "$SESSION_ID" --argjson count "$BLOCK_COUNT" \
      '.block_count = $count | .last_block_session = $sid')
    save_config "$CONFIG"
    
    log "Blocking (count: $BLOCK_COUNT): $INCOMPLETE_COUNT incomplete"
    
    TASK_LIST=$(echo "$TODOS_JSON" | jq -r '
      ([.[] | select(.status == "in_progress") | "  → [in progress] \(.content)"] +
       [.[] | select(.status == "pending") | "  ○ [pending] \(.content)"]) |
      join("\n")
    ')
    
    REASON="You have $INCOMPLETE_COUNT incomplete todo(s):
    $TASK_LIST
    
    Complete these tasks before stopping.
    Mark each task as 'completed' using TodoWrite when done."
    
    jq -n --arg reason "$REASON" '{"decision": "block", "reason": $reason}'
    
    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 withjarrodwatts-claude-code-config

my personal claude code configuration - mostly not created by me, but sourced from many talented people in the community.

Get the whole plugin