Skip to content
Development
Hook

Hooks

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

From plugin
director-mode-lite
8036 skills14 agents6 hooks

Claude Code

> /plugin marketplace add claude-world/director-mode-lite
> /plugin install director-mode-lite@director-mode-lite

Where it lives

  • hooks/_lib-changelog.shGitHub
    Read the script
    #!/bin/bash
    # Changelog Logger - Core logging functions for observability
    # Director Mode Lite
    #
    # This script provides the core logging functionality.
    # Called by other hooks to record events.
    #
    # Claude Code 2.1.9+ Features:
    #   - ${CLAUDE_SESSION_ID} for session tracking
    #   - Session-scoped event logging
    #
    # Note: This is experimental. Hook interface may change in future Claude Code versions.
    
    # Don't exit on errors - logging should never break the main flow
    set +e
    
    CHANGELOG_DIR=".director-mode"
    CHANGELOG_FILE="$CHANGELOG_DIR/changelog.jsonl"
    # Configurable via environment variable
    MAX_LINES="${DIRECTOR_MODE_MAX_CHANGELOG_LINES:-500}"
    
    # Session ID from Claude Code 2.1.9+ (fallback to "default" for older versions)
    SESSION_ID="${CLAUDE_SESSION_ID:-default}"
    
    # Check if jq is available
    HAS_JQ=false
    if command -v jq &>/dev/null; then
        HAS_JQ=true
    fi
    
    # JSON parse helper (with jq fallback)
    json_get() {
        local json="$1"
        local key="$2"
        
        if $HAS_JQ; then
            echo "$json" | jq -r "$key // empty" 2>/dev/null
        else
            # Fallback: basic grep/sed parsing (handles simple cases)
            # This is not a full JSON parser, but handles our use cases
            local simple_key="${key#.}"  # Remove leading dot
            simple_key="${simple_key%%.*}"  # Get first key only
            echo "$json" | grep -o "\"$simple_key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:.*"\([^"]*\)".*/\1/' 2>/dev/null
        fi
    }
    
    # Ensure directory exists
    ensure_dir() {
        mkdir -p "$CHANGELOG_DIR" 2>/dev/null || true
    }
    
    # Generate event ID
    generate_id() {
        echo "evt_$(date +%s)_$RANDOM"
    }
    
    # Get current timestamp
    get_timestamp() {
        date -u +"%Y-%m-%dT%H:%M:%S.000Z" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ"
    }
    
    # Get current iteration (if auto-loop is active)
    get_iteration() {
        if [[ -f ".auto-loop/iteration.txt" ]]; then
            cat ".auto-loop/iteration.txt" 2>/dev/null || echo "null"
        else
            echo "null"
        fi
    }
    
    # Rotate changelog if too large
    rotate_if_needed() {
        if [[ ! -f "$CHANGELOG_FILE" ]]; then
            return 0
        fi
        
        local line_count
        line_count=$(wc -l < "$CHANGELOG_FILE" 2>/dev/null | tr -d ' ') || line_count=0
        
        if [[ "$line_count" -gt "$MAX_LINES" ]]; then
            local archive_name
            archive_name="changelog.$(date +%Y%m%d_%H%M%S).jsonl"
            mv "$CHANGELOG_FILE" "$CHANGELOG_DIR/$archive_name" 2>/dev/null || true
            # Log rotation event
            local ts
            ts=$(get_timestamp)
            echo "{\"id\":\"evt_rotation\",\"timestamp\":\"$ts\",\"event_type\":\"changelog_rotated\",\"agent\":\"system\",\"iteration\":null,\"summary\":\"Rotated to $archive_name\",\"files\":[]}" > "$CHANGELOG_FILE" 2>/dev/null || true
        fi
    }
    
    # Escape string for JSON
    escape_json() {
        local str="$1"
        # Escape backslash first, then other special characters
        str="${str//\\/\\\\}"
        str="${str//\"/\\\"}"
        str="${str//$'\n'/\\n}"
        str="${str//$'\t'/\\t}"
        str="${str//$'\r'/\\r}"
        str="${str//$'\b'/\\b}"
        str="${str//$'\f'/\\f}"
        # Truncate to max length (consistent with log-bash-event.sh)
        echo "${str:0:100}"
    }
    
    # Log an event to changelog
    # Usage: log_event <event_type> <summary> [agent] [files_json]
    log_event() {
        local event_type="${1:-unknown}"
        local summary="${2:-}"
        local agent="${3:-system}"
        local files="${4:-[]}"
    
        ensure_dir
        rotate_if_needed
    
        local id timestamp iteration
        id=$(generate_id)
        timestamp=$(get_timestamp)
        iteration=$(get_iteration)
        local session_id="${SESSION_ID:-default}"
    
        # Escape summary for JSON
        summary=$(escape_json "$summary")
    
        # Build and append event (includes session_id for Claude Code 2.1.9+)
        echo "{\"id\":\"$id\",\"timestamp\":\"$timestamp\",\"session_id\":\"$session_id\",\"event_type\":\"$event_type\",\"agent\":\"$agent\",\"iteration\":$iteration,\"summary\":\"$summary\",\"files\":$files}" >> "$CHANGELOG_FILE" 2>/dev/null || true
    }
    
    # Archive current changelog
    archive_changelog() {
        if [[ -f "$CHANGELOG_FILE" ]]; then
            local line_count
            line_count=$(wc -l < "$CHANGELOG_FILE" 2>/dev/null | tr -d ' ') || line_count=0
            
            if [[ "$line_count" -gt 0 ]]; then
                local archive_name
                archive_name="changelog.$(date +%Y%m%d_%H%M%S).jsonl"
                mv "$CHANGELOG_FILE" "$CHANGELOG_DIR/$archive_name" 2>/dev/null
                echo "Archived to $CHANGELOG_DIR/$archive_name"
            fi
        fi
    }
    
    # Clear changelog
    clear_changelog() {
        rm -f "$CHANGELOG_FILE" 2>/dev/null
        echo "Changelog cleared"
    }
    
    # Export functions for sourcing
    export -f ensure_dir generate_id get_timestamp get_iteration log_event rotate_if_needed archive_changelog clear_changelog json_get escape_json 2>/dev/null || true
    export CHANGELOG_DIR CHANGELOG_FILE MAX_LINES HAS_JQ 2>/dev/null || true
    
  • hooks/advisory.shGitHub
    Read the script
    #!/usr/bin/env bash
    # Guidance-only hook shared by Claude Code, Codex CLI, and Grok Build.
    # Claude and Codex consume its SessionStart context. Current Grok releases
    # ignore passive-hook stdout and instead read the same guidance from AGENTS.md.
    # The script consumes stdin, never denies an action, and always exits zero.
    
    set +e
    CLI_NAME="${1:-cli}"
    payload="$(cat 2>/dev/null || true)"
    : "$payload"
    
    project_dir="${CLAUDE_PROJECT_DIR:-${GROK_WORKSPACE_ROOT:-}}"
    if [[ -z "$project_dir" ]]; then
        project_dir="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
    fi
    latest="$project_dir/.director-mode/handoffs/latest.md"
    message="Director Mode is guidance-only. For substantial work, read .director-mode/GUIDANCE.md and keep outcome, context, constraints, and evidence visible."
    if [[ -f "$latest" ]]; then
        message="$message A portable handoff is available at .director-mode/handoffs/latest.md; verify the worktree before continuing."
    fi
    
    case "$CLI_NAME" in
        claude|grok)
            if command -v python3 >/dev/null 2>&1; then
                DML_MESSAGE="$message" DML_CLI="$CLI_NAME" python3 - <<'PY'
    import json
    import os
    
    print(json.dumps({
        "systemMessage": os.environ["DML_MESSAGE"],
        "hookSpecificOutput": {
            "hookEventName": "SessionStart",
            "additionalContext": os.environ["DML_MESSAGE"],
        },
    }))
    PY
            else
                printf '%s\n' "$message"
            fi
            ;;
        codex)
            if command -v python3 >/dev/null 2>&1; then
                DML_MESSAGE="$message" python3 - <<'PY'
    import json
    import os
    
    print(json.dumps({
        "systemMessage": os.environ["DML_MESSAGE"],
        "hookSpecificOutput": {
            "hookEventName": "SessionStart",
            "additionalContext": os.environ["DML_MESSAGE"],
        },
    }))
    PY
            else
                printf '%s\n' "$message"
            fi
            ;;
        *)
            printf '%s\n' "$message"
            ;;
    esac
    
    exit 0
    
  • hooks/auto-loop-stop.shGitHub
    Read the script
    #!/bin/bash
    # Auto-Loop Stop Hook - TDD-based autonomous loop
    # Director Mode Lite
    #
    # Note: This hook uses `set -euo pipefail` (strict mode) unlike other hooks
    # because it controls the auto-loop continuation logic and must fail fast
    # on any errors to avoid infinite loops or corrupted state.
    #
    # JSON handling uses jq when available (quoting-safe, atomic); a grep/sed
    # fallback keeps jq-less installs working (install.sh warns about jq).
    
    set -euo pipefail
    
    STATE_DIR=".auto-loop"
    CHECKPOINT_FILE="$STATE_DIR/checkpoint.json"
    ITERATION_FILE="$STATE_DIR/iteration.txt"
    STOP_FILE="$STATE_DIR/stop"
    
    # Check if auto-loop is active
    if [[ ! -f "$CHECKPOINT_FILE" ]]; then
        # No active loop, allow normal exit
        exit 0
    fi
    
    # Check for stop signal
    if [[ -f "$STOP_FILE" ]]; then
        rm -f "$STOP_FILE"
        exit 0
    fi
    
    HAS_JQ=false
    command -v jq &>/dev/null && HAS_JQ=true
    
    # Read checkpoint
    if ! checkpoint=$(cat "$CHECKPOINT_FILE" 2>/dev/null); then
        exit 0
    fi
    
    # Parse checkpoint fields
    if $HAS_JQ; then
        status=$(jq -r '.status // "unknown"' "$CHECKPOINT_FILE")
        current_iteration=$(jq -r '.current_iteration // 0' "$CHECKPOINT_FILE")
        max_iterations=$(jq -r '.max_iterations // 20' "$CHECKPOINT_FILE")
        request=$(jq -r '.request // ""' "$CHECKPOINT_FILE")
    else
        status=$(echo "$checkpoint" | grep -o '"status"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4 || echo "unknown")
        current_iteration=$(echo "$checkpoint" | grep -o '"current_iteration"[[:space:]]*:[[:space:]]*[0-9]*' | grep -o '[0-9]*$' || echo "0")
        max_iterations=$(echo "$checkpoint" | grep -o '"max_iterations"[[:space:]]*:[[:space:]]*[0-9]*' | grep -o '[0-9]*$' || echo "20")
        request=$(echo "$checkpoint" | grep -o '"request"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4 || echo "")
    fi
    
    # Check if completed or max iterations reached
    if [[ "$status" == "completed" ]]; then
        exit 0
    fi
    
    if [[ "$current_iteration" -ge "$max_iterations" ]]; then
        # Update status and allow exit
        if $HAS_JQ; then
            jq '.status = "max_iterations_reached"' "$CHECKPOINT_FILE" > "$CHECKPOINT_FILE.tmp" \
                && mv "$CHECKPOINT_FILE.tmp" "$CHECKPOINT_FILE"
        else
            echo "$checkpoint" | sed 's/"status"[[:space:]]*:[[:space:]]*"[^"]*"/"status": "max_iterations_reached"/' > "$CHECKPOINT_FILE"
        fi
        exit 0
    fi
    
    # Increment iteration
    new_iteration=$((current_iteration + 1))
    echo "$new_iteration" > "$ITERATION_FILE"
    
    # Update checkpoint (atomic with jq)
    if $HAS_JQ; then
        jq ".current_iteration = $new_iteration" "$CHECKPOINT_FILE" > "$CHECKPOINT_FILE.tmp" \
            && mv "$CHECKPOINT_FILE.tmp" "$CHECKPOINT_FILE"
    else
        echo "$checkpoint" | sed "s/\"current_iteration\"[[:space:]]*:[[:space:]]*[0-9]*/\"current_iteration\": $new_iteration/" > "$CHECKPOINT_FILE"
    fi
    
    # Extract AC status for prompt
    if $HAS_JQ; then
        ac_status=$(jq -r 'if (.acceptance_criteria // []) | length == 0 then "No AC defined"
            else .acceptance_criteria[] | (if .done then "[x] " else "[ ] " end) + (.description // "Unknown") end' \
            "$CHECKPOINT_FILE" 2>/dev/null || echo "Check .auto-loop/checkpoint.json")
    else
        ac_status=$(echo "$checkpoint" | python3 -c "
    import json, sys
    try:
        data = json.load(sys.stdin)
        acs = data.get('acceptance_criteria', [])
        if not acs:
            print('No AC defined')
        else:
            for ac in acs:
                mark = '[x]' if ac.get('done') else '[ ]'
                print(f\"{mark} {ac.get('description', 'Unknown')}\")
    except:
        print('Unable to parse AC')
    " 2>/dev/null || echo "Check .auto-loop/checkpoint.json")
    fi
    
    # Build TDD prompt for next iteration
    tdd_prompt="Continue Auto-Loop iteration #$new_iteration / $max_iterations
    
    Original request: $request
    
    Acceptance Criteria status:
    $ac_status
    
    Follow the TDD cycle:
    1. RED - Write a failing test for an incomplete AC
    2. GREEN - Implement code to make the test pass
    3. REFACTOR - Improve code quality (keep tests passing)
    4. VALIDATE - Run lint and tests
    5. COMMIT - Commit successful changes
    6. DECIDE - Update the corresponding AC's done status in checkpoint.json
    
    If all ACs are complete, update status to \"completed\"."
    
    # Block stop with reason for next iteration (official Stop-hook schema)
    if $HAS_JQ; then
        jq -n --arg reason "$tdd_prompt" '{decision: "block", reason: $reason}'
    else
        json_reason=$(echo "$tdd_prompt" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null) || {
            json_reason="\"Continue Auto-Loop iteration #$new_iteration\""
        }
        cat <<EOF
    {
      "decision": "block",
      "reason": $json_reason
    }
    EOF
    fi
    
  • hooks/log-bash-event.shGitHub
    Read the script
    #!/bin/bash
    # Log Bash Event Hook - Records test results and git commits
    # Director Mode Lite
    #
    # PostToolUse hook for Bash tool
    # Detects test runs and git commits, logs to changelog
    #
    # Input: JSON via stdin (Claude Code PostToolUse format)
    # Output: None (exit 0 per Hooks guide)
    #
    # Note: This single hook handles both tests and commits to avoid stdin conflicts
    
    # Never exit on errors
    set +e
    
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" || SCRIPT_DIR="$(pwd)/.claude/hooks"
    
    # Check for jq availability (before sourcing, in case source fails)
    HAS_JQ=false
    command -v jq &>/dev/null && HAS_JQ=true
    
    # Source the logger library
    if [[ -f "$SCRIPT_DIR/_lib-changelog.sh" ]]; then
        source "$SCRIPT_DIR/_lib-changelog.sh"
    elif [[ -f ".claude/hooks/_lib-changelog.sh" ]]; then
        source ".claude/hooks/_lib-changelog.sh"
    else
        # Minimal inline fallback if changelog-logger.sh not found (includes session_id for Claude Code 2.1.9+)
        log_event() {
            mkdir -p ".director-mode" 2>/dev/null
            local ts
            ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ")
            local iter="null"
            local sid="${CLAUDE_SESSION_ID:-default}"
            [[ -f ".auto-loop/iteration.txt" ]] && iter=$(cat ".auto-loop/iteration.txt" 2>/dev/null || echo "null")
            echo "{\"id\":\"evt_$(date +%s)_$RANDOM\",\"timestamp\":\"$ts\",\"session_id\":\"$sid\",\"event_type\":\"$1\",\"agent\":\"$3\",\"iteration\":$iter,\"summary\":\"$2\",\"files\":$4}" >> ".director-mode/changelog.jsonl" 2>/dev/null
        }
    fi
    
    # Read JSON from stdin ONCE
    INPUT=$(cat 2>/dev/null) || INPUT=""
    [[ -z "$INPUT" ]] && exit 0
    
    # Parse fields
    if $HAS_JQ; then
        TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) || TOOL_NAME=""
        COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) || COMMAND=""
        # PostToolUse delivers the Bash result as tool_response {stdout, stderr, ...};
        # a plain string and the legacy tool_output key are still accepted.
        OUTPUT=$(echo "$INPUT" | jq -r '(.tool_response // .tool_output // empty)
            | if type == "object" then ([.stdout, .stderr] | map(strings | select(length > 0)) | join("\n"))
              elif type == "string" then . else empty end' 2>/dev/null) || OUTPUT=""
    else
        TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\([^"]*\)".*/\1/' 2>/dev/null) || TOOL_NAME=""
        COMMAND=$(echo "$INPUT" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\([^"]*\)".*/\1/' 2>/dev/null) || COMMAND=""
        OUTPUT=""
    fi
    
    # Only process Bash tool
    [[ "$TOOL_NAME" != "Bash" ]] && exit 0
    [[ -z "$COMMAND" ]] && exit 0
    
    # ============================================================
    # Check if this is a TEST command
    # ============================================================
    is_test_command() {
        local cmd="$1"
        # npm/yarn/pnpm
        [[ "$cmd" =~ (npm|yarn|pnpm)[[:space:]]+(test|run[[:space:]]+test) ]] && return 0
        [[ "$cmd" =~ (npm|yarn|pnpm)[[:space:]]+run[[:space:]]+(test:|test-|test$) ]] && return 0
        # Direct test runners
        [[ "$cmd" =~ (npx|yarn|pnpm)[[:space:]]+(jest|vitest|mocha|ava) ]] && return 0
        [[ "$cmd" =~ ^[[:space:]]*(pytest|jest|vitest|mocha|ava)[[:space:]] ]] && return 0
        [[ "$cmd" =~ ^[[:space:]]*(pytest|jest|vitest|mocha)$ ]] && return 0
        # Language-specific
        [[ "$cmd" =~ ^[[:space:]]*go[[:space:]]+test ]] && return 0
        [[ "$cmd" =~ ^[[:space:]]*cargo[[:space:]]+test ]] && return 0
        [[ "$cmd" =~ ^[[:space:]]*mix[[:space:]]+test ]] && return 0
        [[ "$cmd" =~ ^[[:space:]]*rspec ]] && return 0
        [[ "$cmd" =~ ^[[:space:]]*phpunit ]] && return 0
        [[ "$cmd" =~ ^[[:space:]]*python.*-m[[:space:]]+(unittest|pytest) ]] && return 0
        [[ "$cmd" =~ ^[[:space:]]*node[[:space:]]+--test ]] && return 0
        [[ "$cmd" =~ ^[[:space:]]*deno[[:space:]]+test ]] && return 0
        [[ "$cmd" =~ ^[[:space:]]*bun[[:space:]]+test ]] && return 0
        # Build tools
        [[ "$cmd" =~ ^[[:space:]]*make[[:space:]]+test ]] && return 0
        [[ "$cmd" =~ ^[[:space:]]*gradle[[:space:]]+test ]] && return 0
        [[ "$cmd" =~ ^[[:space:]]*mvn[[:space:]]+test ]] && return 0
        [[ "$cmd" =~ ^[[:space:]]*dotnet[[:space:]]+test ]] && return 0
        return 1
    }
    
    # ============================================================
    # Check if this is a GIT COMMIT command
    # ============================================================
    is_commit_command() {
        local cmd="$1"
        [[ "$cmd" =~ git[[:space:]]+commit ]] && return 0
        return 1
    }
    
    # ============================================================
    # Handle TEST command
    # ============================================================
    handle_test() {
        local output="$1"
        
        # Detect result
        local result="unknown"
        if [[ -n "$output" ]]; then
            if [[ "$output" =~ (FAIL|FAILED|failed|failure|Error:|AssertionError|✗|✕|[0-9]+[[:space:]]+failing) ]]; then
                result="fail"
            elif [[ "$output" =~ (PASS|PASSED|passed|success|✓|✔|[0-9]+[[:space:]]+passing|All[[:space:]]+tests[[:space:]]+passed|OK) ]]; then
                result="pass"
            fi
        fi
        
        # Set event type and summary
        local event_type="test_run"
        local summary="Tests executed"
        
        case "$result" in
            pass)
                event_type="test_pass"
                summary="Tests passing"
                ;;
            fail)
                event_type="test_fail"
                summary="Tests failing"
                ;;
        esac
        
        # Try to extract counts
        if [[ -n "$output" ]]; then
            if [[ "$output" =~ ([0-9]+)[[:space:]]+(passed|passing) ]]; then
                local passed="${BASH_REMATCH[1]}"
                summary="$passed tests passing"
            fi
            if [[ "$output" =~ ([0-9]+)[[:space:]]+(failed|failing) ]]; then
                local failed="${BASH_REMATCH[1]}"
                if [[ "$event_type" == "test_fail" ]]; then
                    summary="$failed tests failing"
                fi
            fi
  • hooks/log-file-change.shGitHub
    Read the script
    #!/bin/bash
    # Log File Change Hook - Records Write/Edit operations
    # Director Mode Lite
    #
    # PostToolUse hook for Write and Edit tools
    # Automatically logs file changes to the changelog
    #
    # Input: JSON via stdin (Claude Code PostToolUse format)
    # Output: None (exit 0 per Hooks guide)
    
    # Never exit on errors - don't break the main flow
    set +e
    
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" || SCRIPT_DIR="$(pwd)/.claude/hooks"
    
    # Source the logger library
    if [[ -f "$SCRIPT_DIR/_lib-changelog.sh" ]]; then
        source "$SCRIPT_DIR/_lib-changelog.sh"
    elif [[ -f ".claude/hooks/_lib-changelog.sh" ]]; then
        source ".claude/hooks/_lib-changelog.sh"
    else
        # Minimal inline fallback (includes session_id for Claude Code 2.1.9+)
        log_event() {
            mkdir -p ".director-mode" 2>/dev/null
            local ts
            ts=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ")
            local iter="null"
            local sid="${CLAUDE_SESSION_ID:-default}"
            [[ -f ".auto-loop/iteration.txt" ]] && iter=$(cat ".auto-loop/iteration.txt" 2>/dev/null || echo "null")
            echo "{\"id\":\"evt_$(date +%s)_$RANDOM\",\"timestamp\":\"$ts\",\"session_id\":\"$sid\",\"event_type\":\"$1\",\"agent\":\"$3\",\"iteration\":$iter,\"summary\":\"$2\",\"files\":$4}" >> ".director-mode/changelog.jsonl" 2>/dev/null
        }
        HAS_JQ=false
        command -v jq &>/dev/null && HAS_JQ=true
    fi
    
    # Read JSON from stdin (Claude Code PostToolUse format)
    INPUT=$(cat 2>/dev/null) || INPUT=""
    
    # Exit if no input
    [[ -z "$INPUT" ]] && exit 0
    
    # Parse tool name and file path
    if $HAS_JQ; then
        TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) || TOOL_NAME=""
        FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null) || FILE_PATH=""
    else
        # Fallback: grep parsing
        TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\([^"]*\)".*/\1/' 2>/dev/null) || TOOL_NAME=""
        FILE_PATH=$(echo "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\([^"]*\)".*/\1/' 2>/dev/null) || FILE_PATH=""
    fi
    
    # Exit if we couldn't parse tool name
    [[ -z "$TOOL_NAME" ]] && exit 0
    
    # Determine event type
    # Note: Write tool overwrites files, so we use "file_write" (not "file_created")
    # since we can't know if the file existed before without PreToolUse context
    case "$TOOL_NAME" in
        Write)
            EVENT_TYPE="file_write"
            ;;
        Edit)
            EVENT_TYPE="file_edit"
            ;;
        *)
            # Not a file change tool
            exit 0
            ;;
    esac
    
    # Build summary and files JSON
    if [[ -n "$FILE_PATH" ]]; then
        FILENAME=$(basename "$FILE_PATH" 2>/dev/null) || FILENAME="$FILE_PATH"
        SUMMARY="$EVENT_TYPE: $FILENAME"
        # Escape file path for JSON
        FILE_PATH_ESCAPED="${FILE_PATH//\\/\\\\}"
        FILE_PATH_ESCAPED="${FILE_PATH_ESCAPED//\"/\\\"}"
        FILES_JSON="[\"$FILE_PATH_ESCAPED\"]"
    else
        SUMMARY="$EVENT_TYPE: unknown file"
        FILES_JSON="[]"
    fi
    
    # Log the event
    log_event "$EVENT_TYPE" "$SUMMARY" "hook" "$FILES_JSON"
    
    exit 0
    
  • hooks/pre-tool-validator.shGitHub
    Read the script
    #!/bin/bash
    # Pre-Tool Validator Hook - Adds context for protected files
    # Director Mode Lite
    #
    # PreToolUse hook for Write and Edit tools
    # Returns additionalContext to guide Claude about sensitive files
    #
    # Input: JSON via stdin (Claude Code PreToolUse format)
    # Output: JSON with decision field (required) + optional additionalContext
    #
    # Note: This hook provides guidance, not blocks. It adds context to help
    # Claude make better decisions about sensitive file modifications.
    
    # Never exit on errors
    set +e
    
    # Check for jq availability
    HAS_JQ=false
    command -v jq &>/dev/null && HAS_JQ=true
    
    # Read JSON from stdin
    INPUT=$(cat 2>/dev/null) || INPUT=""
    [[ -z "$INPUT" ]] && exit 0
    
    # Parse tool name and file path
    if $HAS_JQ; then
        TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) || TOOL_NAME=""
        FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null) || FILE_PATH=""
    else
        TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\([^"]*\)".*/\1/' 2>/dev/null) || TOOL_NAME=""
        FILE_PATH=$(echo "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\([^"]*\)".*/\1/' 2>/dev/null) || FILE_PATH=""
    fi
    
    # Only process Write and Edit tools
    case "$TOOL_NAME" in
        Write|Edit) ;;
        *) exit 0 ;;
    esac
    
    # Exit if no file path
    [[ -z "$FILE_PATH" ]] && exit 0
    
    # Get filename for pattern matching
    FILENAME=$(basename "$FILE_PATH" 2>/dev/null) || FILENAME="$FILE_PATH"
    
    # Define protected file patterns and their guidance
    get_additional_context() {
        local path="$1"
        local name="$2"
    
        # Environment files - contain secrets
        if [[ "$name" =~ ^\.env(\.|$) ]] || [[ "$name" == ".env" ]]; then
            echo "This is an environment file that may contain secrets. Never commit secrets to git. Use placeholder values if creating examples."
            return
        fi
    
        # Claude settings - may break tool
        if [[ "$path" =~ \.claude/settings\.local\.json$ ]] || [[ "$path" =~ \.claude/settings\.json$ ]]; then
            echo "This is a Claude Code settings file. Invalid JSON will break Claude Code. Ensure proper JSON format."
            return
        fi
    
        # Package lock files - usually auto-generated
        if [[ "$name" == "package-lock.json" ]] || [[ "$name" == "yarn.lock" ]] || [[ "$name" == "pnpm-lock.yaml" ]]; then
            echo "This is an auto-generated lockfile. Usually should not be manually edited. Use npm/yarn/pnpm commands instead."
            return
        fi
    
        # Git internal files
        if [[ "$path" =~ ^\.git/ ]] || [[ "$path" =~ /\.git/ ]]; then
            echo "This is a git internal file. Direct modification may corrupt the repository."
            return
        fi
    
        # CI/CD files
        if [[ "$path" =~ \.github/workflows/ ]] || [[ "$name" == ".gitlab-ci.yml" ]] || [[ "$name" == "Jenkinsfile" ]]; then
            echo "This is a CI/CD configuration file. Changes will affect automated pipelines. Test changes carefully."
            return
        fi
    
        # Docker files
        if [[ "$name" == "Dockerfile" ]] || [[ "$name" == "docker-compose.yml" ]] || [[ "$name" == "docker-compose.yaml" ]]; then
            echo "This is a Docker configuration file. Ensure base images are from trusted sources and no secrets are hardcoded."
            return
        fi
    
        # Credentials/auth files
        if [[ "$name" =~ credentials ]] || [[ "$name" =~ (^|\.)auth\. ]] || [[ "$name" =~ \.pem$ ]] || [[ "$name" =~ \.key$ ]]; then
            echo "This appears to be a credentials or key file. Never commit real credentials. Use environment variables or secrets management."
            return
        fi
    
        # Database migrations
        if [[ "$path" =~ migrations/ ]] || [[ "$path" =~ migrate/ ]]; then
            echo "This is a database migration file. Once deployed, migrations should not be modified. Create new migrations instead."
            return
        fi
    
        # No special context needed
        echo ""
    }
    
    # Get context for this file
    CONTEXT=$(get_additional_context "$FILE_PATH" "$FILENAME")
    
    # Output format per Claude Code Hooks guide:
    # - Allow without context: exit 0 (no output)
    # - Add context: {"hookSpecificOutput": {"hookEventName": "PreToolUse", "additionalContext": "..."}}
    if [[ -n "$CONTEXT" ]]; then
        # Escape for JSON
        CONTEXT="${CONTEXT//\\/\\\\}"
        CONTEXT="${CONTEXT//\"/\\\"}"
        CONTEXT="${CONTEXT//$'\n'/\\n}"
        echo "{\"hookSpecificOutput\": {\"hookEventName\": \"PreToolUse\", \"additionalContext\": \"$CONTEXT\"}}"
    fi
    
    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.

@claude-world
Ships withdirector-mode-lite

Use Claude Code like a Director, not a Programmer. MIT toolkit with Auto-Loop, guided setup, 27 commands, 14 agents, and 32 skills.

8036 skills14 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
80
Star history of claude-world/director-mode-lite on GitHub, a chart by star-history.com

More plugins that ship hooks.

superpowers

superpowers

A development methodology for coding agents that makes the agent brainstorm before building, follow plans, run parallel work and finish branches in a disciplined way. You get fewer skipped steps and more consistent results from your coding assistant.

15 skills1 hook311k
@obra@obra
Install Plugin
ponytail

ponytail

A Claude Code plugin with skills for auditing a whole repo, reviewing a change, listing deferred shortcut comments, and showing measured savings and a quick command reference.

6 skills10 hooks188k
@dietrichgebert@dietrichgebert
Install Plugin
agent-skills

agent-skills

Walks a software change from a written spec through design, testing, code review, CI/CD, and release, applying the matching engineering skill at each stage. Useful for developers using Claude Code on full development workflows.

25 skills+3112k
@addyosmani@addyosmani
Install Plugin
planning-with-files

planning-with-files

Persistent file-based planning for AI coding agents on long-running, multi-step tasks. It keeps task_plan.md, findings.md and progress.md on disk, so work can resume after /clear or context compaction.

6 skills+228k
@othmanadi@othmanadi
Install Plugin
gsd-core

gsd-core

A spec-driven development workflow for Claude Code that guides a project through planning, phases, milestone audits and fixes. Includes skills to generate tests from UAT criteria and to audit completed work.

72 skills+311k
@open-gsd@open-gsd
Install Plugin
superpowers-zh

superpowers-zh

Chinese-language skill pack for Claude Code that adds workflow guidance for brainstorming, TDD, debugging, and code review, plus Chinese conventions for commits, documentation, and domestic Git platforms like Gitee and Coding.net.

21 skills1 hook8.5k
@jnmetacode@jnmetacode
Install Plugin
socraticode

socraticode

Gives Claude Code semantic search, dependency graph and architecture context over a codebase, so an AI assistant can explore and understand a project's code. Includes skills for indexing a project and managing its file watching.

2 skills+33.4k
@giancarloerra@giancarloerra
Install Plugin
cocoindex-code

cocoindex-code

A lightweight code search engine that uses AST-based semantic search with local embeddings and incremental indexing. It helps Claude Code users find code faster and more efficiently.

1 skill1 MCP+13k
@cocoindex-io@cocoindex-io
Install Plugin

Questions about the hooks in director-mode-lite.

Which coding agents can use the hooks in director-mode-lite?
The hooks in director-mode-lite install in Claude Code and Cursor: each runs Claude Code hooks from .claude/settings.json, so copy the hooks there.