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.
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

