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
8136 skills14 agents6 hooks
Install
> /plugin marketplace add claude-world/director-mode-lite
> /plugin install director-mode-lite@director-mode-lite

Ships with director-mode-lite. Installing the plugin gets these hooks.

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="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=$(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=$(generate_id)
        local timestamp=$(get_timestamp)
        local 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="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=$(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=""
        OUTPUT=$(echo "$INPUT" | jq -r '.tool_output // empty' 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
        fi
        
        log_event "$event_type" "$summary" "hook" "[]"
    }
    
    # ============================================================
    # Handle COMMIT command
    # ============================================================
    handle_commit() {
        local cmd="$1"
        local output="$2"
        
        local commit_msg=""
        
        # Extract commit message from -
  • 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=$(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.

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.

Get the whole plugin