{
  "identity": {
    "name": "disler-hooks-tts",
    "version": "latest",
    "publisher": {
      "name": "disler"
    },
    "source": "https://github.com/disler/claude-code-hooks-mastery/tree/main/.claude/settings.json",
    "license": "unknown",
    "type": "skill",
    "subtype": "hook"
  },
  "interface_": {
    "format": "hook_json",
    "triggers": {
      "activation": "Binds to lifecycle events: PreToolUse, PostToolUse, Notification, Stop, SubagentStop, UserPromptSubmit, PreCompact, SessionStart, SessionEnd, PermissionRequest, PostToolUseFailure, SubagentStart, Setup",
      "scope": "Hook configuration with 13 handler(s)"
    },
    "instruction_count": 13,
    "file_count": 23,
    "hook_events": {
      "PreToolUse": 1,
      "PostToolUse": 1,
      "Notification": 1,
      "Stop": 1,
      "SubagentStop": 1,
      "UserPromptSubmit": 1,
      "PreCompact": 1,
      "SessionStart": 1,
      "SessionEnd": 1,
      "PermissionRequest": 1,
      "PostToolUseFailure": 1,
      "SubagentStart": 1,
      "Setup": 1
    },
    "hook_handlers": {
      "command": 13
    },
    "agent_context": {
      "required_capabilities": [],
      "required_tools": [],
      "expected_context": "Claude Code session with hook support"
    }
  },
  "behavioralGuarantees": {
    "does": [],
    "does_not": [],
    "scope_boundaries": {
      "modifies_files": false,
      "creates_files": false,
      "deletes_files": false,
      "makes_network_requests": false,
      "accesses_env_variables": true,
      "invokes_external_tools": true
    },
    "failure_modes": []
  },
  "dependencies": [],
  "author_description": "Claude Code hooks from the hooks-mastery suite providing AI-generated TTS audio feedback, UserPromptSubmit prompt validation and enhancement, PreToolUse security blocks, and structured session logging.",
  "sourceHash": "sha256:cf47483616cad9af7e4aaa887fbef75183312c14147625c81ed82d9ae7a8a969",
  "buildStatus": {
    "installed": false,
    "built": true
  },
  "extractedAt": "2026-04-01T03:23:45.466Z",
  "extractionMethod": "hook_static",
  "confidence": 1,
  "durationMs": 39,
  "hook_metadata": {
    "hook_events": {
      "PreToolUse": 1,
      "PostToolUse": 1,
      "Notification": 1,
      "Stop": 1,
      "SubagentStop": 1,
      "UserPromptSubmit": 1,
      "PreCompact": 1,
      "SessionStart": 1,
      "SessionEnd": 1,
      "PermissionRequest": 1,
      "PostToolUseFailure": 1,
      "SubagentStart": 1,
      "Setup": 1
    },
    "hook_handlers": {
      "command": 13
    },
    "commands": [
      "uv run $CLAUDE_PROJECT_DIR/.claude/hooks/pre_tool_use.py",
      "uv run $CLAUDE_PROJECT_DIR/.claude/hooks/post_tool_use.py",
      "uv run $CLAUDE_PROJECT_DIR/.claude/hooks/notification.py --notify",
      "uv run $CLAUDE_PROJECT_DIR/.claude/hooks/stop.py --chat",
      "uv run $CLAUDE_PROJECT_DIR/.claude/hooks/subagent_stop.py --notify",
      "uv run $CLAUDE_PROJECT_DIR/.claude/hooks/user_prompt_submit.py --log-only --store-last-prompt --name-agent",
      "uv run $CLAUDE_PROJECT_DIR/.claude/hooks/pre_compact.py",
      "uv run $CLAUDE_PROJECT_DIR/.claude/hooks/session_start.py",
      "uv run $CLAUDE_PROJECT_DIR/.claude/hooks/session_end.py",
      "uv run $CLAUDE_PROJECT_DIR/.claude/hooks/permission_request.py --log-only",
      "uv run $CLAUDE_PROJECT_DIR/.claude/hooks/post_tool_use_failure.py",
      "uv run $CLAUDE_PROJECT_DIR/.claude/hooks/subagent_start.py",
      "uv run $CLAUDE_PROJECT_DIR/.claude/hooks/setup.py"
    ],
    "prompts": [],
    "matchers": [],
    "total_hooks": 13,
    "companion_scripts": [
      {
        "name": "notification.py",
        "path": "hooks/notification.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\nimport argparse\r\nimport json\r\nimport os\r\nimport sys\r\nimport subprocess\r\nimport random\r\nfrom pathlib import Path\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\ndef get_tts_script_path():\r\n    \"\"\"\r\n    Determine which TTS script to use based on available API keys.\r\n    Priority order: ElevenLabs > OpenAI > pyttsx3\r\n    \"\"\"\r\n    # Get current script directory and construct utils/tts path\r\n    script_dir = Path(__file__).parent\r\n    tts_dir = script_dir / \"utils\" / \"tts\"\r\n    \r\n    # Check for ElevenLabs API key (highest priority)\r\n    if os.getenv('ELEVENLABS_API_KEY'):\r\n        elevenlabs_script = tts_dir / \"elevenlabs_tts.py\"\r\n        if elevenlabs_script.exists():\r\n            return str(elevenlabs_script)\r\n    \r\n    # Check for OpenAI API key (second priority)\r\n    if os.getenv('OPENAI_API_KEY'):\r\n        openai_script = tts_dir / \"openai_tts.py\"\r\n        if openai_script.exists():\r\n            return str(openai_script)\r\n    \r\n    # Fall back to pyttsx3 (no API key required)\r\n    pyttsx3_script = tts_dir / \"pyttsx3_tts.py\"\r\n    if pyttsx3_script.exists():\r\n        return str(pyttsx3_script)\r\n    \r\n    return None\r\n\r\n\r\ndef announce_notification():\r\n    \"\"\"Announce that the agent needs user input.\"\"\"\r\n    try:\r\n        tts_script = get_tts_script_path()\r\n        if not tts_script:\r\n            return  # No TTS scripts available\r\n        \r\n        # Get engineer name if available\r\n        engineer_name = os.getenv('ENGINEER_NAME', '').strip()\r\n        \r\n        # Create notification message with 30% chance to include name\r\n        if engineer_name and random.random() < 0.3:\r\n            notification_message = f\"{engineer_name}, your agent needs your input\"\r\n        else:\r\n            notification_message = \"Your agent needs your input\"\r\n        \r\n        # Call the TTS script with the notification message\r\n        subprocess.run([\r\n            \"uv\", \"run\", tts_script, notification_message\r\n        ], \r\n        capture_output=True,  # Suppress output\r\n        timeout=10  # 10-second timeout\r\n        )\r\n        \r\n    except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):\r\n        # Fail silently if TTS encounters issues\r\n        pass\r\n    except Exception:\r\n        # Fail silently for any other errors\r\n        pass\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Parse command line arguments\r\n        parser = argparse.ArgumentParser()\r\n        parser.add_argument('--notify', action='store_true', help='Enable TTS notifications')\r\n        args = parser.parse_args()\r\n        \r\n        # Read JSON input from stdin\r\n        input_data = json.loads(sys.stdin.read())\r\n        \r\n        # Ensure log directory exists\r\n        import os\r\n        log_dir = os.path.join(os.getcwd(), 'logs')\r\n        os.makedirs(log_dir, exist_ok=True)\r\n        log_file = os.path.join(log_dir, 'notification.json')\r\n        \r\n        # Read existing log data or initialize empty list\r\n        if os.path.exists(log_file):\r\n            with open(log_file, 'r') as f:\r\n                try:\r\n                    log_data = json.load(f)\r\n                except (json.JSONDecodeError, ValueError):\r\n                    log_data = []\r\n        else:\r\n            log_data = []\r\n        \r\n        # Append new data\r\n        log_data.append(input_data)\r\n        \r\n        # Write back to file with formatting\r\n        with open(log_file, 'w') as f:\r\n            json.dump(log_data, f, indent=2)\r\n        \r\n        # Announce notification via TTS only if --notify flag is set\r\n        # Skip TTS for the generic \"Claude is waiting for your input\" message\r\n        if args.notify and input_data.get('message') != 'Claude is waiting for your input':\r\n            announce_notification()\r\n        \r\n        sys.exit(0)\r\n        \r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully\r\n        sys.exit(0)\r\n    except Exception:\r\n        # Handle any other errors gracefully\r\n        sys.exit(0)\r\n\r\nif __name__ == '__main__':\r\n    main()",
        "size": 4195
      },
      {
        "name": "permission_request.py",
        "path": "hooks/permission_request.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\n\"\"\"\r\nPermissionRequest Hook\r\n======================\r\nTriggered when the user is shown a permission dialog.\r\n\r\nThis hook can:\r\n- Log all permission requests for auditing\r\n- Auto-allow specific patterns (e.g., read-only operations)\r\n- Deny permission requests based on security policies\r\n- Modify tool inputs before allowing\r\n\r\nInput JSON includes:\r\n- session_id, transcript_path, cwd, permission_mode\r\n- hook_event_name: \"PermissionRequest\"\r\n- tool_name, tool_input, tool_use_id\r\n\r\nOutput JSON for decision control:\r\n{\r\n  \"hookSpecificOutput\": {\r\n    \"hookEventName\": \"PermissionRequest\",\r\n    \"decision\": {\r\n      \"behavior\": \"allow\" | \"deny\",\r\n      \"updatedInput\": {...},  // optional for allow\r\n      \"message\": \"...\",       // optional for deny\r\n      \"interrupt\": false      // optional for deny\r\n    }\r\n  }\r\n}\r\n\"\"\"\r\n\r\nimport argparse\r\nimport json\r\nimport re\r\nimport sys\r\nfrom pathlib import Path\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\n# Read-only patterns that can be auto-allowed\r\nREAD_ONLY_PATTERNS = {\r\n    \"Read\": lambda tool_input: True,  # All Read operations\r\n    \"Glob\": lambda tool_input: True,  # All Glob operations\r\n    \"Grep\": lambda tool_input: True,  # All Grep operations\r\n    \"Bash\": lambda tool_input: is_safe_bash_command(tool_input.get(\"command\", \"\")),\r\n}\r\n\r\n# Safe bash commands that can be auto-allowed\r\nSAFE_BASH_COMMANDS = [\r\n    r\"^ls\\b\",\r\n    r\"^pwd\\b\",\r\n    r\"^echo\\b\",\r\n    r\"^cat\\b(?!.*>)\",  # cat without redirection\r\n    r\"^head\\b\",\r\n    r\"^tail\\b\",\r\n    r\"^wc\\b\",\r\n    r\"^which\\b\",\r\n    r\"^whereis\\b\",\r\n    r\"^type\\b\",\r\n    r\"^file\\b\",\r\n    r\"^stat\\b\",\r\n    r\"^git\\s+(status|log|diff|show|branch|tag)\\b\",\r\n    r\"^git\\s+remote\\s+-v\\b\",\r\n    r\"^npm\\s+(list|ls|outdated|view)\\b\",\r\n    r\"^pip\\s+(list|show|freeze)\\b\",\r\n    r\"^uv\\s+(pip\\s+list|tree)\\b\",\r\n    r\"^python\\s+--version\\b\",\r\n    r\"^node\\s+--version\\b\",\r\n    r\"^npm\\s+--version\\b\",\r\n]\r\n\r\n\r\ndef is_safe_bash_command(command: str) -> bool:\r\n    \"\"\"\r\n    Check if a bash command is safe (read-only).\r\n\r\n    Args:\r\n        command: The bash command to check\r\n\r\n    Returns:\r\n        True if the command is considered safe/read-only\r\n    \"\"\"\r\n    if not command:\r\n        return False\r\n\r\n    # Normalize command\r\n    normalized = command.strip()\r\n\r\n    # Check against safe patterns\r\n    for pattern in SAFE_BASH_COMMANDS:\r\n        if re.search(pattern, normalized):\r\n            return True\r\n\r\n    return False\r\n\r\n\r\ndef should_auto_allow(tool_name: str, tool_input: dict) -> bool:\r\n    \"\"\"\r\n    Determine if a tool call should be auto-allowed based on read-only patterns.\r\n\r\n    Args:\r\n        tool_name: Name of the tool being called\r\n        tool_input: The tool's input parameters\r\n\r\n    Returns:\r\n        True if the tool call should be auto-allowed\r\n    \"\"\"\r\n    if tool_name in READ_ONLY_PATTERNS:\r\n        check_func = READ_ONLY_PATTERNS[tool_name]\r\n        return check_func(tool_input)\r\n\r\n    return False\r\n\r\n\r\ndef get_auto_allow_reason(tool_name: str, tool_input: dict) -> str:\r\n    \"\"\"\r\n    Get a reason string for why a tool was auto-allowed.\r\n\r\n    Args:\r\n        tool_name: Name of the tool being called\r\n        tool_input: The tool's input parameters\r\n\r\n    Returns:\r\n        Human-readable reason for auto-allowing\r\n    \"\"\"\r\n    if tool_name == \"Read\":\r\n        file_path = tool_input.get(\"file_path\", \"unknown\")\r\n        return f\"Read operation auto-allowed: {file_path}\"\r\n    elif tool_name == \"Glob\":\r\n        pattern = tool_input.get(\"pattern\", \"unknown\")\r\n        return f\"Glob pattern search auto-allowed: {pattern}\"\r\n    elif tool_name == \"Grep\":\r\n        pattern = tool_input.get(\"pattern\", \"unknown\")\r\n        return f\"Grep search auto-allowed: {pattern}\"\r\n    elif tool_name == \"Bash\":\r\n        command = tool_input.get(\"command\", \"unknown\")\r\n        return f\"Safe bash command auto-allowed: {command[:50]}...\"\r\n\r\n    return f\"{tool_name} auto-allowed (read-only operation)\"\r\n\r\n\r\ndef create_allow_response(updated_input: dict | None = None, reason: str | None = None) -> dict:\r\n    \"\"\"\r\n    Create a JSON response to allow a permission request.\r\n\r\n    Args:\r\n        updated_input: Optional modified tool input\r\n        reason: Optional reason for allowing (for logging)\r\n\r\n    Returns:\r\n        JSON-serializable response dict\r\n    \"\"\"\r\n    decision = {\"behavior\": \"allow\"}\r\n\r\n    if updated_input is not None:\r\n        decision[\"updatedInput\"] = updated_input\r\n\r\n    return {\r\n        \"hookSpecificOutput\": {\r\n            \"hookEventName\": \"PermissionRequest\",\r\n            \"decision\": decision\r\n        }\r\n    }\r\n\r\n\r\ndef create_deny_response(message: str, interrupt: bool = False) -> dict:\r\n    \"\"\"\r\n    Create a JSON response to deny a permission request.\r\n\r\n    Args:\r\n        message: Message explaining why permission was denied\r\n        interrupt: Whether to stop Claude after denying\r\n\r\n    Returns:\r\n        JSON-serializable response dict\r\n    \"\"\"\r\n    return {\r\n        \"hookSpecificOutput\": {\r\n            \"hookEventName\": \"PermissionRequest\",\r\n            \"decision\": {\r\n                \"behavior\": \"deny\",\r\n                \"message\": message,\r\n                \"interrupt\": interrupt\r\n            }\r\n        }\r\n    }\r\n\r\n\r\ndef log_permission_request(input_data: dict, log_dir: Path):\r\n    \"\"\"\r\n    Log the permission request to a JSON file.\r\n\r\n    Args:\r\n        input_data: The input data from the hook\r\n        log_dir: Path to the logs directory\r\n    \"\"\"\r\n    log_path = log_dir / \"permission_request.json\"\r\n\r\n    # Read existing log data or initialize empty list\r\n    if log_path.exists():\r\n        with open(log_path, \"r\") as f:\r\n            try:\r\n                log_data = json.load(f)\r\n            except (json.JSONDecodeError, ValueError):\r\n                log_data = []\r\n    else:\r\n        log_data = []\r\n\r\n    # Append new data\r\n    log_data.append(input_data)\r\n\r\n    # Write back to file with formatting\r\n    with open(log_path, \"w\") as f:\r\n        json.dump(log_data, f, indent=2)\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Parse command line arguments\r\n        parser = argparse.ArgumentParser(\r\n            description=\"PermissionRequest hook for Claude Code\"\r\n        )\r\n        parser.add_argument(\r\n            \"--auto-allow\",\r\n            action=\"store_true\",\r\n            help=\"Auto-allow read-only operations (Read, Glob, Grep, safe Bash commands)\"\r\n        )\r\n        parser.add_argument(\r\n            \"--log-only\",\r\n            action=\"store_true\",\r\n            help=\"Only log permission requests, do not make decisions\"\r\n        )\r\n        args = parser.parse_args()\r\n\r\n        # Read JSON input from stdin\r\n        input_data = json.load(sys.stdin)\r\n\r\n        # Extract fields\r\n        tool_name = input_data.get(\"tool_name\", \"\")\r\n        tool_input = input_data.get(\"tool_input\", {})\r\n        hook_event_name = input_data.get(\"hook_event_name\", \"\")\r\n\r\n        # Verify this is a PermissionRequest event\r\n        if hook_event_name != \"PermissionRequest\":\r\n            # Not a PermissionRequest event, exit gracefully\r\n            sys.exit(0)\r\n\r\n        # Ensure log directory exists\r\n        log_dir = Path.cwd() / \"logs\"\r\n        log_dir.mkdir(parents=True, exist_ok=True)\r\n\r\n        # Log the permission request\r\n        log_permission_request(input_data, log_dir)\r\n\r\n        # If log-only mode, exit without making a decision\r\n        if args.log_only:\r\n            sys.exit(0)\r\n\r\n        # Handle auto-allow for read-only operations\r\n        if args.auto_allow and should_auto_allow(tool_name, tool_input):\r\n            reason = get_auto_allow_reason(tool_name, tool_input)\r\n            response = create_allow_response(reason=reason)\r\n            print(json.dumps(response))\r\n            sys.exit(0)\r\n\r\n        # Default: exit without making a decision (let user decide)\r\n        sys.exit(0)\r\n\r\n    except json.JSONDecodeError:\r\n        # Gracefully handle JSON decode errors\r\n        sys.exit(0)\r\n    except Exception:\r\n        # Handle any other errors gracefully\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    main()\r\n",
        "size": 8205
      },
      {
        "name": "post_tool_use.py",
        "path": "hooks/post_tool_use.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.8\"\r\n# ///\r\n\r\nimport json\r\nimport os\r\nimport sys\r\nfrom pathlib import Path\r\n\r\ndef main():\r\n    try:\r\n        # Read JSON input from stdin\r\n        input_data = json.load(sys.stdin)\r\n        \r\n        # Ensure log directory exists\r\n        log_dir = Path.cwd() / 'logs'\r\n        log_dir.mkdir(parents=True, exist_ok=True)\r\n        log_path = log_dir / 'post_tool_use.json'\r\n        \r\n        # Read existing log data or initialize empty list\r\n        if log_path.exists():\r\n            with open(log_path, 'r') as f:\r\n                try:\r\n                    log_data = json.load(f)\r\n                except (json.JSONDecodeError, ValueError):\r\n                    log_data = []\r\n        else:\r\n            log_data = []\r\n        \r\n        # Append new data\r\n        log_data.append(input_data)\r\n        \r\n        # Write back to file with formatting\r\n        with open(log_path, 'w') as f:\r\n            json.dump(log_data, f, indent=2)\r\n        \r\n        sys.exit(0)\r\n        \r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully\r\n        sys.exit(0)\r\n    except Exception:\r\n        # Exit cleanly on any other error\r\n        sys.exit(0)\r\n\r\nif __name__ == '__main__':\r\n    main()",
        "size": 1281
      },
      {
        "name": "post_tool_use_failure.py",
        "path": "hooks/post_tool_use_failure.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\nimport json\r\nimport sys\r\nfrom datetime import datetime\r\nfrom pathlib import Path\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Read JSON input from stdin\r\n        input_data = json.load(sys.stdin)\r\n\r\n        # Add timestamp to the log entry\r\n        input_data['logged_at'] = datetime.now().isoformat()\r\n\r\n        # Extract key fields for enhanced logging\r\n        tool_name = input_data.get('tool_name', 'unknown')\r\n        tool_use_id = input_data.get('tool_use_id', 'unknown')\r\n        error = input_data.get('error', {})\r\n\r\n        # Create a structured log entry with error details\r\n        log_entry = {\r\n            'timestamp': input_data['logged_at'],\r\n            'session_id': input_data.get('session_id', ''),\r\n            'hook_event_name': input_data.get('hook_event_name', 'PostToolUseFailure'),\r\n            'tool_name': tool_name,\r\n            'tool_use_id': tool_use_id,\r\n            'tool_input': input_data.get('tool_input', {}),\r\n            'error': error,\r\n            'cwd': input_data.get('cwd', ''),\r\n            'permission_mode': input_data.get('permission_mode', ''),\r\n            'transcript_path': input_data.get('transcript_path', ''),\r\n            'raw_input': input_data\r\n        }\r\n\r\n        # Ensure log directory exists\r\n        log_dir = Path.cwd() / 'logs'\r\n        log_dir.mkdir(parents=True, exist_ok=True)\r\n        log_path = log_dir / 'post_tool_use_failure.json'\r\n\r\n        # Read existing log data or initialize empty list\r\n        if log_path.exists():\r\n            with open(log_path, 'r') as f:\r\n                try:\r\n                    log_data = json.load(f)\r\n                except (json.JSONDecodeError, ValueError):\r\n                    log_data = []\r\n        else:\r\n            log_data = []\r\n\r\n        # Append new log entry\r\n        log_data.append(log_entry)\r\n\r\n        # Write back to file with formatting\r\n        with open(log_path, 'w') as f:\r\n            json.dump(log_data, f, indent=2)\r\n\r\n        sys.exit(0)\r\n\r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully\r\n        sys.exit(0)\r\n    except Exception:\r\n        # Exit cleanly on any other error\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == '__main__':\r\n    main()\r\n",
        "size": 2456
      },
      {
        "name": "pre_compact.py",
        "path": "hooks/pre_compact.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\nimport argparse\r\nimport json\r\nimport os\r\nimport sys\r\nfrom pathlib import Path\r\nfrom datetime import datetime\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\ndef log_pre_compact(input_data):\r\n    \"\"\"Log pre-compact event to logs directory.\"\"\"\r\n    # Ensure logs directory exists\r\n    log_dir = Path(\"logs\")\r\n    log_dir.mkdir(parents=True, exist_ok=True)\r\n    log_file = log_dir / 'pre_compact.json'\r\n    \r\n    # Read existing log data or initialize empty list\r\n    if log_file.exists():\r\n        with open(log_file, 'r') as f:\r\n            try:\r\n                log_data = json.load(f)\r\n            except (json.JSONDecodeError, ValueError):\r\n                log_data = []\r\n    else:\r\n        log_data = []\r\n    \r\n    # Append the entire input data\r\n    log_data.append(input_data)\r\n    \r\n    # Write back to file with formatting\r\n    with open(log_file, 'w') as f:\r\n        json.dump(log_data, f, indent=2)\r\n\r\n\r\ndef backup_transcript(transcript_path, trigger):\r\n    \"\"\"Create a backup of the transcript before compaction.\"\"\"\r\n    try:\r\n        if not os.path.exists(transcript_path):\r\n            return\r\n        \r\n        # Create backup directory\r\n        backup_dir = Path(\"logs\") / \"transcript_backups\"\r\n        backup_dir.mkdir(parents=True, exist_ok=True)\r\n        \r\n        # Generate backup filename with timestamp and trigger type\r\n        timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\r\n        session_name = Path(transcript_path).stem\r\n        backup_name = f\"{session_name}_pre_compact_{trigger}_{timestamp}.jsonl\"\r\n        backup_path = backup_dir / backup_name\r\n        \r\n        # Copy transcript to backup\r\n        import shutil\r\n        shutil.copy2(transcript_path, backup_path)\r\n        \r\n        return str(backup_path)\r\n    except Exception:\r\n        return None\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Parse command line arguments\r\n        parser = argparse.ArgumentParser()\r\n        parser.add_argument('--backup', action='store_true',\r\n                          help='Create backup of transcript before compaction')\r\n        parser.add_argument('--verbose', action='store_true',\r\n                          help='Print verbose output')\r\n        args = parser.parse_args()\r\n        \r\n        # Read JSON input from stdin\r\n        input_data = json.loads(sys.stdin.read())\r\n        \r\n        # Extract fields\r\n        session_id = input_data.get('session_id', 'unknown')\r\n        transcript_path = input_data.get('transcript_path', '')\r\n        trigger = input_data.get('trigger', 'unknown')  # \"manual\" or \"auto\"\r\n        custom_instructions = input_data.get('custom_instructions', '')\r\n        \r\n        # Log the pre-compact event\r\n        log_pre_compact(input_data)\r\n        \r\n        # Create backup if requested\r\n        backup_path = None\r\n        if args.backup and transcript_path:\r\n            backup_path = backup_transcript(transcript_path, trigger)\r\n        \r\n        # Provide feedback based on trigger type\r\n        if args.verbose:\r\n            if trigger == \"manual\":\r\n                message = f\"Preparing for manual compaction (session: {session_id[:8]}...)\"\r\n                if custom_instructions:\r\n                    message += f\"\\nCustom instructions: {custom_instructions[:100]}...\"\r\n            else:  # auto\r\n                message = f\"Auto-compaction triggered due to full context window (session: {session_id[:8]}...)\"\r\n            \r\n            if backup_path:\r\n                message += f\"\\nTranscript backed up to: {backup_path}\"\r\n            \r\n            print(message)\r\n        \r\n        # Success - compaction will proceed\r\n        sys.exit(0)\r\n        \r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully\r\n        sys.exit(0)\r\n    except Exception:\r\n        # Handle any other errors gracefully\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == '__main__':\r\n    main()",
        "size": 4066
      },
      {
        "name": "pre_tool_use.py",
        "path": "hooks/pre_tool_use.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.8\"\r\n# ///\r\n\r\nimport json\r\nimport sys\r\nimport re\r\nfrom pathlib import Path\r\n\r\ndef is_dangerous_rm_command(command):\r\n    \"\"\"\r\n    Comprehensive detection of dangerous rm commands.\r\n    Matches various forms of rm -rf and similar destructive patterns.\r\n    \"\"\"\r\n    # Normalize command by removing extra spaces and converting to lowercase\r\n    normalized = ' '.join(command.lower().split())\r\n    \r\n    # Pattern 1: Standard rm -rf variations\r\n    patterns = [\r\n        r'\\brm\\s+.*-[a-z]*r[a-z]*f',  # rm -rf, rm -fr, rm -Rf, etc.\r\n        r'\\brm\\s+.*-[a-z]*f[a-z]*r',  # rm -fr variations\r\n        r'\\brm\\s+--recursive\\s+--force',  # rm --recursive --force\r\n        r'\\brm\\s+--force\\s+--recursive',  # rm --force --recursive\r\n        r'\\brm\\s+-r\\s+.*-f',  # rm -r ... -f\r\n        r'\\brm\\s+-f\\s+.*-r',  # rm -f ... -r\r\n    ]\r\n    \r\n    # Check for dangerous patterns\r\n    for pattern in patterns:\r\n        if re.search(pattern, normalized):\r\n            return True\r\n    \r\n    # Pattern 2: Check for rm with recursive flag targeting dangerous paths\r\n    dangerous_paths = [\r\n        r'/',           # Root directory\r\n        r'/\\*',         # Root with wildcard\r\n        r'~',           # Home directory\r\n        r'~/',          # Home directory path\r\n        r'\\$HOME',      # Home environment variable\r\n        r'\\.\\.',        # Parent directory references\r\n        r'\\*',          # Wildcards in general rm -rf context\r\n        r'\\.',          # Current directory\r\n        r'\\.\\s*$',      # Current directory at end of command\r\n    ]\r\n    \r\n    if re.search(r'\\brm\\s+.*-[a-z]*r', normalized):  # If rm has recursive flag\r\n        for path in dangerous_paths:\r\n            if re.search(path, normalized):\r\n                return True\r\n    \r\n    return False\r\n\r\ndef is_env_file_access(tool_name, tool_input):\r\n    \"\"\"\r\n    Check if any tool is trying to access .env files containing sensitive data.\r\n    \"\"\"\r\n    if tool_name in ['Read', 'Edit', 'MultiEdit', 'Write', 'Bash']:\r\n        # Check file paths for file-based tools\r\n        if tool_name in ['Read', 'Edit', 'MultiEdit', 'Write']:\r\n            file_path = tool_input.get('file_path', '')\r\n            if '.env' in file_path and not file_path.endswith('.env.sample'):\r\n                return True\r\n        \r\n        # Check bash commands for .env file access\r\n        elif tool_name == 'Bash':\r\n            command = tool_input.get('command', '')\r\n            # Pattern to detect .env file access (but allow .env.sample)\r\n            env_patterns = [\r\n                r'\\b\\.env\\b(?!\\.sample)',  # .env but not .env.sample\r\n                r'cat\\s+.*\\.env\\b(?!\\.sample)',  # cat .env\r\n                r'echo\\s+.*>\\s*\\.env\\b(?!\\.sample)',  # echo > .env\r\n                r'touch\\s+.*\\.env\\b(?!\\.sample)',  # touch .env\r\n                r'cp\\s+.*\\.env\\b(?!\\.sample)',  # cp .env\r\n                r'mv\\s+.*\\.env\\b(?!\\.sample)',  # mv .env\r\n            ]\r\n            \r\n            for pattern in env_patterns:\r\n                if re.search(pattern, command):\r\n                    return True\r\n    \r\n    return False\r\n\r\ndef main():\r\n    try:\r\n        # Read JSON input from stdin\r\n        input_data = json.load(sys.stdin)\r\n        \r\n        tool_name = input_data.get('tool_name', '')\r\n        tool_input = input_data.get('tool_input', {})\r\n        \r\n        # Check for .env file access (blocks access to sensitive environment files)\r\n        if is_env_file_access(tool_name, tool_input):\r\n            print(\"BLOCKED: Access to .env files containing sensitive data is prohibited\", file=sys.stderr)\r\n            print(\"Use .env.sample for template files instead\", file=sys.stderr)\r\n            sys.exit(2)  # Exit code 2 blocks tool call and shows error to Claude\r\n        \r\n        # Check for dangerous rm -rf commands\r\n        if tool_name == 'Bash':\r\n            command = tool_input.get('command', '')\r\n            \r\n            # Block rm -rf commands with comprehensive pattern matching\r\n            if is_dangerous_rm_command(command):\r\n                print(\"BLOCKED: Dangerous rm command detected and prevented\", file=sys.stderr)\r\n                sys.exit(2)  # Exit code 2 blocks tool call and shows error to Claude\r\n        \r\n        # Ensure log directory exists\r\n        log_dir = Path.cwd() / 'logs'\r\n        log_dir.mkdir(parents=True, exist_ok=True)\r\n        log_path = log_dir / 'pre_tool_use.json'\r\n        \r\n        # Read existing log data or initialize empty list\r\n        if log_path.exists():\r\n            with open(log_path, 'r') as f:\r\n                try:\r\n                    log_data = json.load(f)\r\n                except (json.JSONDecodeError, ValueError):\r\n                    log_data = []\r\n        else:\r\n            log_data = []\r\n        \r\n        # Append new data\r\n        log_data.append(input_data)\r\n        \r\n        # Write back to file with formatting\r\n        with open(log_path, 'w') as f:\r\n            json.dump(log_data, f, indent=2)\r\n        \r\n        sys.exit(0)\r\n        \r\n    except json.JSONDecodeError:\r\n        # Gracefully handle JSON decode errors\r\n        sys.exit(0)\r\n    except Exception:\r\n        # Handle any other errors gracefully\r\n        sys.exit(0)\r\n\r\nif __name__ == '__main__':\r\n    main()",
        "size": 5283
      },
      {
        "name": "session_end.py",
        "path": "hooks/session_end.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\nimport argparse\r\nimport json\r\nimport sys\r\nfrom pathlib import Path\r\nfrom datetime import datetime\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\ndef log_session_end(input_data):\r\n    \"\"\"Log session end event to logs directory.\"\"\"\r\n    # Ensure logs directory exists\r\n    log_dir = Path(\"logs\")\r\n    log_dir.mkdir(parents=True, exist_ok=True)\r\n    log_file = log_dir / 'session_end.json'\r\n\r\n    # Read existing log data or initialize empty list\r\n    if log_file.exists():\r\n        with open(log_file, 'r') as f:\r\n            try:\r\n                log_data = json.load(f)\r\n            except (json.JSONDecodeError, ValueError):\r\n                log_data = []\r\n    else:\r\n        log_data = []\r\n\r\n    # Add timestamp to the input data\r\n    input_data['logged_at'] = datetime.now().isoformat()\r\n\r\n    # Append the entire input data\r\n    log_data.append(input_data)\r\n\r\n    # Write back to file with formatting\r\n    with open(log_file, 'w') as f:\r\n        json.dump(log_data, f, indent=2)\r\n\r\n\r\ndef perform_cleanup():\r\n    \"\"\"Perform optional cleanup tasks at session end.\"\"\"\r\n    cleanup_actions = []\r\n\r\n    # Example cleanup: Remove temporary files from logs directory\r\n    log_dir = Path(\"logs\")\r\n    if log_dir.exists():\r\n        # Clean up any .tmp files\r\n        for tmp_file in log_dir.glob(\"*.tmp\"):\r\n            try:\r\n                tmp_file.unlink()\r\n                cleanup_actions.append(f\"Removed temp file: {tmp_file.name}\")\r\n            except Exception:\r\n                pass\r\n\r\n    # Example cleanup: Clean up old chat.json if it exists and is stale\r\n    chat_file = log_dir / \"chat.json\" if log_dir.exists() else None\r\n    if chat_file and chat_file.exists():\r\n        try:\r\n            # Check if file is older than 24 hours\r\n            file_age = datetime.now().timestamp() - chat_file.stat().st_mtime\r\n            if file_age > 86400:  # 24 hours in seconds\r\n                chat_file.unlink()\r\n                cleanup_actions.append(\"Removed stale chat.json (older than 24 hours)\")\r\n        except Exception:\r\n            pass\r\n\r\n    return cleanup_actions\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Parse command line arguments\r\n        parser = argparse.ArgumentParser()\r\n        parser.add_argument('--cleanup', action='store_true',\r\n                          help='Perform cleanup tasks at session end')\r\n        args = parser.parse_args()\r\n\r\n        # Read JSON input from stdin\r\n        input_data = json.loads(sys.stdin.read())\r\n\r\n        # Extract session_id for cleanup logging\r\n        session_id = input_data.get('session_id', 'unknown')\r\n\r\n        # Log the session end event\r\n        log_session_end(input_data)\r\n\r\n        # Perform cleanup if requested\r\n        if args.cleanup:\r\n            cleanup_actions = perform_cleanup()\r\n            if cleanup_actions:\r\n                # Log cleanup actions\r\n                cleanup_log = {\r\n                    \"session_id\": session_id,\r\n                    \"cleanup_at\": datetime.now().isoformat(),\r\n                    \"actions\": cleanup_actions\r\n                }\r\n                log_dir = Path(\"logs\")\r\n                cleanup_file = log_dir / \"cleanup.json\"\r\n\r\n                # Read existing cleanup log\r\n                if cleanup_file.exists():\r\n                    with open(cleanup_file, 'r') as f:\r\n                        try:\r\n                            cleanup_data = json.load(f)\r\n                        except (json.JSONDecodeError, ValueError):\r\n                            cleanup_data = []\r\n                else:\r\n                    cleanup_data = []\r\n\r\n                cleanup_data.append(cleanup_log)\r\n\r\n                with open(cleanup_file, 'w') as f:\r\n                    json.dump(cleanup_data, f, indent=2)\r\n\r\n        # Success\r\n        sys.exit(0)\r\n\r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully\r\n        sys.exit(0)\r\n    except Exception:\r\n        # Handle any other errors gracefully\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == '__main__':\r\n    main()\r\n",
        "size": 4200
      },
      {
        "name": "session_start.py",
        "path": "hooks/session_start.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\nimport argparse\r\nimport json\r\nimport os\r\nimport sys\r\nimport subprocess\r\nfrom pathlib import Path\r\nfrom datetime import datetime\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\ndef log_session_start(input_data):\r\n    \"\"\"Log session start event to logs directory.\"\"\"\r\n    # Ensure logs directory exists\r\n    log_dir = Path(\"logs\")\r\n    log_dir.mkdir(parents=True, exist_ok=True)\r\n    log_file = log_dir / 'session_start.json'\r\n    \r\n    # Read existing log data or initialize empty list\r\n    if log_file.exists():\r\n        with open(log_file, 'r') as f:\r\n            try:\r\n                log_data = json.load(f)\r\n            except (json.JSONDecodeError, ValueError):\r\n                log_data = []\r\n    else:\r\n        log_data = []\r\n    \r\n    # Append the entire input data\r\n    log_data.append(input_data)\r\n    \r\n    # Write back to file with formatting\r\n    with open(log_file, 'w') as f:\r\n        json.dump(log_data, f, indent=2)\r\n\r\n\r\ndef get_git_status():\r\n    \"\"\"Get current git status information.\"\"\"\r\n    try:\r\n        # Get current branch\r\n        branch_result = subprocess.run(\r\n            ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],\r\n            capture_output=True,\r\n            text=True,\r\n            timeout=5\r\n        )\r\n        current_branch = branch_result.stdout.strip() if branch_result.returncode == 0 else \"unknown\"\r\n        \r\n        # Get uncommitted changes count\r\n        status_result = subprocess.run(\r\n            ['git', 'status', '--porcelain'],\r\n            capture_output=True,\r\n            text=True,\r\n            timeout=5\r\n        )\r\n        if status_result.returncode == 0:\r\n            changes = status_result.stdout.strip().split('\\n') if status_result.stdout.strip() else []\r\n            uncommitted_count = len(changes)\r\n        else:\r\n            uncommitted_count = 0\r\n        \r\n        return current_branch, uncommitted_count\r\n    except Exception:\r\n        return None, None\r\n\r\n\r\ndef get_recent_issues():\r\n    \"\"\"Get recent GitHub issues if gh CLI is available.\"\"\"\r\n    try:\r\n        # Check if gh is available\r\n        gh_check = subprocess.run(['which', 'gh'], capture_output=True)\r\n        if gh_check.returncode != 0:\r\n            return None\r\n        \r\n        # Get recent open issues\r\n        result = subprocess.run(\r\n            ['gh', 'issue', 'list', '--limit', '5', '--state', 'open'],\r\n            capture_output=True,\r\n            text=True,\r\n            timeout=10\r\n        )\r\n        if result.returncode == 0 and result.stdout.strip():\r\n            return result.stdout.strip()\r\n    except Exception:\r\n        pass\r\n    return None\r\n\r\n\r\ndef load_development_context(source):\r\n    \"\"\"Load relevant development context based on session source.\"\"\"\r\n    context_parts = []\r\n    \r\n    # Add timestamp\r\n    context_parts.append(f\"Session started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\r\n    context_parts.append(f\"Session source: {source}\")\r\n    \r\n    # Add git information\r\n    branch, changes = get_git_status()\r\n    if branch:\r\n        context_parts.append(f\"Git branch: {branch}\")\r\n        if changes > 0:\r\n            context_parts.append(f\"Uncommitted changes: {changes} files\")\r\n    \r\n    # Load project-specific context files if they exist\r\n    context_files = [\r\n        \".claude/CONTEXT.md\",\r\n        \".claude/TODO.md\",\r\n        \"TODO.md\",\r\n        \".github/ISSUE_TEMPLATE.md\"\r\n    ]\r\n    \r\n    for file_path in context_files:\r\n        if Path(file_path).exists():\r\n            try:\r\n                with open(file_path, 'r') as f:\r\n                    content = f.read().strip()\r\n                    if content:\r\n                        context_parts.append(f\"\\n--- Content from {file_path} ---\")\r\n                        context_parts.append(content[:1000])  # Limit to first 1000 chars\r\n            except Exception:\r\n                pass\r\n    \r\n    # Add recent issues if available\r\n    issues = get_recent_issues()\r\n    if issues:\r\n        context_parts.append(\"\\n--- Recent GitHub Issues ---\")\r\n        context_parts.append(issues)\r\n    \r\n    return \"\\n\".join(context_parts)\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Parse command line arguments\r\n        parser = argparse.ArgumentParser()\r\n        parser.add_argument('--load-context', action='store_true',\r\n                          help='Load development context at session start')\r\n        parser.add_argument('--announce', action='store_true',\r\n                          help='Announce session start via TTS')\r\n        args = parser.parse_args()\r\n        \r\n        # Read JSON input from stdin\r\n        input_data = json.loads(sys.stdin.read())\r\n        \r\n        # Extract fields\r\n        session_id = input_data.get('session_id', 'unknown')\r\n        source = input_data.get('source', 'unknown')  # \"startup\", \"resume\", or \"clear\"\r\n        \r\n        # Log the session start event\r\n        log_session_start(input_data)\r\n        \r\n        # Load development context if requested\r\n        if args.load_context:\r\n            context = load_development_context(source)\r\n            if context:\r\n                # Using JSON output to add context\r\n                output = {\r\n                    \"hookSpecificOutput\": {\r\n                        \"hookEventName\": \"SessionStart\",\r\n                        \"additionalContext\": context\r\n                    }\r\n                }\r\n                print(json.dumps(output))\r\n                sys.exit(0)\r\n        \r\n        # Announce session start if requested\r\n        if args.announce:\r\n            try:\r\n                # Try to use TTS to announce session start\r\n                script_dir = Path(__file__).parent\r\n                tts_script = script_dir / \"utils\" / \"tts\" / \"pyttsx3_tts.py\"\r\n                \r\n                if tts_script.exists():\r\n                    messages = {\r\n                        \"startup\": \"Claude Code session started\",\r\n                        \"resume\": \"Resuming previous session\",\r\n                        \"clear\": \"Starting fresh session\"\r\n                    }\r\n                    message = messages.get(source, \"Session started\")\r\n                    \r\n                    subprocess.run(\r\n                        [\"uv\", \"run\", str(tts_script), message],\r\n                        capture_output=True,\r\n                        timeout=5\r\n                    )\r\n            except Exception:\r\n                pass\r\n        \r\n        # Success\r\n        sys.exit(0)\r\n        \r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully\r\n        sys.exit(0)\r\n    except Exception:\r\n        # Handle any other errors gracefully\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == '__main__':\r\n    main()",
        "size": 6841
      },
      {
        "name": "setup.py",
        "path": "hooks/setup.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\nimport argparse\r\nimport json\r\nimport os\r\nimport sys\r\nimport subprocess\r\nfrom pathlib import Path\r\nfrom datetime import datetime\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\ndef log_setup(input_data):\r\n    \"\"\"Log setup event to logs directory.\"\"\"\r\n    # Ensure logs directory exists\r\n    log_dir = Path(\"logs\")\r\n    log_dir.mkdir(parents=True, exist_ok=True)\r\n    log_file = log_dir / 'setup.json'\r\n\r\n    # Read existing log data or initialize empty list\r\n    if log_file.exists():\r\n        with open(log_file, 'r') as f:\r\n            try:\r\n                log_data = json.load(f)\r\n            except (json.JSONDecodeError, ValueError):\r\n                log_data = []\r\n    else:\r\n        log_data = []\r\n\r\n    # Append the entire input data with timestamp\r\n    entry = {\r\n        \"timestamp\": datetime.now().isoformat(),\r\n        **input_data\r\n    }\r\n    log_data.append(entry)\r\n\r\n    # Write back to file with formatting\r\n    with open(log_file, 'w') as f:\r\n        json.dump(log_data, f, indent=2)\r\n\r\n\r\ndef persist_env_variable(name, value):\r\n    \"\"\"Persist an environment variable via CLAUDE_ENV_FILE.\"\"\"\r\n    env_file = os.environ.get('CLAUDE_ENV_FILE')\r\n    if env_file:\r\n        with open(env_file, 'a') as f:\r\n            f.write(f'export {name}=\"{value}\"\\n')\r\n        return True\r\n    return False\r\n\r\n\r\ndef check_dependencies():\r\n    \"\"\"Check if common project dependencies are available.\"\"\"\r\n    deps_status = {}\r\n\r\n    # Check for Node.js / npm\r\n    try:\r\n        result = subprocess.run(['node', '--version'], capture_output=True, text=True, timeout=5)\r\n        deps_status['node'] = result.stdout.strip() if result.returncode == 0 else None\r\n    except Exception:\r\n        deps_status['node'] = None\r\n\r\n    # Check for Python\r\n    try:\r\n        result = subprocess.run(['python3', '--version'], capture_output=True, text=True, timeout=5)\r\n        deps_status['python'] = result.stdout.strip() if result.returncode == 0 else None\r\n    except Exception:\r\n        deps_status['python'] = None\r\n\r\n    # Check for uv\r\n    try:\r\n        result = subprocess.run(['uv', '--version'], capture_output=True, text=True, timeout=5)\r\n        deps_status['uv'] = result.stdout.strip() if result.returncode == 0 else None\r\n    except Exception:\r\n        deps_status['uv'] = None\r\n\r\n    # Check for git\r\n    try:\r\n        result = subprocess.run(['git', '--version'], capture_output=True, text=True, timeout=5)\r\n        deps_status['git'] = result.stdout.strip() if result.returncode == 0 else None\r\n    except Exception:\r\n        deps_status['git'] = None\r\n\r\n    return deps_status\r\n\r\n\r\ndef install_project_dependencies():\r\n    \"\"\"Attempt to install project dependencies based on detected files.\"\"\"\r\n    installed = []\r\n    errors = []\r\n\r\n    # Check for package.json (Node.js)\r\n    if Path('package.json').exists():\r\n        try:\r\n            # Prefer npm ci for CI environments, npm install otherwise\r\n            cmd = ['npm', 'ci'] if Path('package-lock.json').exists() else ['npm', 'install']\r\n            result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)\r\n            if result.returncode == 0:\r\n                installed.append('npm dependencies')\r\n            else:\r\n                errors.append(f'npm: {result.stderr[:200]}')\r\n        except Exception as e:\r\n            errors.append(f'npm: {str(e)}')\r\n\r\n    # Check for requirements.txt (Python)\r\n    if Path('requirements.txt').exists():\r\n        try:\r\n            result = subprocess.run(\r\n                ['pip', 'install', '-r', 'requirements.txt'],\r\n                capture_output=True, text=True, timeout=300\r\n            )\r\n            if result.returncode == 0:\r\n                installed.append('pip dependencies')\r\n            else:\r\n                errors.append(f'pip: {result.stderr[:200]}')\r\n        except Exception as e:\r\n            errors.append(f'pip: {str(e)}')\r\n\r\n    # Check for pyproject.toml (Python with uv or pip)\r\n    if Path('pyproject.toml').exists() and not Path('requirements.txt').exists():\r\n        try:\r\n            # Try uv first\r\n            result = subprocess.run(['uv', 'sync'], capture_output=True, text=True, timeout=300)\r\n            if result.returncode == 0:\r\n                installed.append('uv dependencies')\r\n            else:\r\n                # Fallback to pip install .\r\n                result = subprocess.run(\r\n                    ['pip', 'install', '-e', '.'],\r\n                    capture_output=True, text=True, timeout=300\r\n                )\r\n                if result.returncode == 0:\r\n                    installed.append('pip (pyproject.toml)')\r\n                else:\r\n                    errors.append(f'pyproject.toml: {result.stderr[:200]}')\r\n        except Exception as e:\r\n            errors.append(f'pyproject.toml: {str(e)}')\r\n\r\n    return installed, errors\r\n\r\n\r\ndef get_project_info(cwd):\r\n    \"\"\"Gather project information for context.\"\"\"\r\n    info = []\r\n\r\n    # Check for git repository\r\n    try:\r\n        result = subprocess.run(\r\n            ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],\r\n            capture_output=True, text=True, timeout=5, cwd=cwd\r\n        )\r\n        if result.returncode == 0:\r\n            info.append(f\"Git branch: {result.stdout.strip()}\")\r\n    except Exception:\r\n        pass\r\n\r\n    # Check for common project files\r\n    project_files = [\r\n        ('package.json', 'Node.js project'),\r\n        ('pyproject.toml', 'Python project (pyproject.toml)'),\r\n        ('requirements.txt', 'Python project (requirements.txt)'),\r\n        ('Cargo.toml', 'Rust project'),\r\n        ('go.mod', 'Go project'),\r\n        ('Makefile', 'Makefile present'),\r\n    ]\r\n\r\n    for filename, description in project_files:\r\n        if Path(cwd, filename).exists():\r\n            info.append(f\"Detected: {description}\")\r\n\r\n    # Check for .claude directory\r\n    if Path(cwd, '.claude').exists():\r\n        info.append(\"Claude Code configuration directory present\")\r\n\r\n        # Check for CLAUDE.md or CONTEXT.md\r\n        for context_file in ['CLAUDE.md', 'CONTEXT.md']:\r\n            context_path = Path(cwd, '.claude', context_file)\r\n            if context_path.exists():\r\n                info.append(f\"Found {context_file} in .claude/\")\r\n\r\n    return info\r\n\r\n\r\ndef run_maintenance_tasks(cwd):\r\n    \"\"\"Run periodic maintenance tasks.\"\"\"\r\n    tasks_completed = []\r\n\r\n    # Check disk usage of logs directory\r\n    logs_dir = Path(cwd, 'logs')\r\n    if logs_dir.exists():\r\n        try:\r\n            total_size = sum(f.stat().st_size for f in logs_dir.rglob('*') if f.is_file())\r\n            size_mb = total_size / (1024 * 1024)\r\n            if size_mb > 10:\r\n                tasks_completed.append(f\"Warning: logs directory is {size_mb:.2f}MB\")\r\n            else:\r\n                tasks_completed.append(f\"Logs directory size: {size_mb:.2f}MB\")\r\n        except Exception:\r\n            pass\r\n\r\n    # Run git gc if repository is large\r\n    try:\r\n        result = subprocess.run(\r\n            ['git', 'count-objects', '-v'],\r\n            capture_output=True, text=True, timeout=10, cwd=cwd\r\n        )\r\n        if result.returncode == 0:\r\n            tasks_completed.append(\"Git repository status checked\")\r\n    except Exception:\r\n        pass\r\n\r\n    return tasks_completed\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Parse command line arguments\r\n        parser = argparse.ArgumentParser()\r\n        parser.add_argument('--install-deps', action='store_true',\r\n                          help='Install project dependencies')\r\n        parser.add_argument('--verbose', action='store_true',\r\n                          help='Print verbose output')\r\n        args = parser.parse_args()\r\n\r\n        # Read JSON input from stdin\r\n        input_data = json.loads(sys.stdin.read())\r\n\r\n        # Extract fields from Setup hook input\r\n        session_id = input_data.get('session_id', 'unknown')\r\n        _transcript_path = input_data.get('transcript_path', '')  # noqa: F841\r\n        cwd = input_data.get('cwd', os.getcwd())\r\n        _permission_mode = input_data.get('permission_mode', 'default')  # noqa: F841\r\n        _hook_event_name = input_data.get('hook_event_name', 'Setup')  # noqa: F841\r\n        trigger = input_data.get('trigger', 'init')  # \"init\" or \"maintenance\"\r\n\r\n        # Log the setup event\r\n        log_setup(input_data)\r\n\r\n        # Build context information\r\n        context_parts = []\r\n        context_parts.append(f\"Setup triggered: {trigger}\")\r\n        context_parts.append(f\"Session: {session_id[:8]}...\")\r\n        context_parts.append(f\"Working directory: {cwd}\")\r\n\r\n        # Gather project information\r\n        project_info = get_project_info(cwd)\r\n        if project_info:\r\n            context_parts.append(\"\\n--- Project Information ---\")\r\n            context_parts.extend(project_info)\r\n\r\n        # Check dependencies status\r\n        deps_status = check_dependencies()\r\n        available_deps = [f\"{k}: {v}\" for k, v in deps_status.items() if v]\r\n        if available_deps:\r\n            context_parts.append(\"\\n--- Available Tools ---\")\r\n            context_parts.extend(available_deps)\r\n\r\n        # Handle trigger-specific actions\r\n        if trigger == 'init':\r\n            context_parts.append(\"\\n--- Repository Initialization ---\")\r\n\r\n            # Persist project path as environment variable\r\n            persist_env_variable('PROJECT_ROOT', cwd)\r\n\r\n            # Install dependencies if requested\r\n            if args.install_deps:\r\n                context_parts.append(\"Installing dependencies...\")\r\n                installed, errors = install_project_dependencies()\r\n                if installed:\r\n                    context_parts.append(f\"Installed: {', '.join(installed)}\")\r\n                if errors:\r\n                    context_parts.append(f\"Errors: {'; '.join(errors)}\")\r\n\r\n            context_parts.append(\"Repository initialized with custom configuration\")\r\n\r\n        elif trigger == 'maintenance':\r\n            context_parts.append(\"\\n--- Maintenance Tasks ---\")\r\n\r\n            # Run maintenance tasks\r\n            maintenance_results = run_maintenance_tasks(cwd)\r\n            if maintenance_results:\r\n                context_parts.extend(maintenance_results)\r\n\r\n            context_parts.append(\"Maintenance tasks completed\")\r\n\r\n        # Prepare JSON output with additionalContext\r\n        context = \"\\n\".join(context_parts)\r\n\r\n        output = {\r\n            \"hookSpecificOutput\": {\r\n                \"hookEventName\": \"Setup\",\r\n                \"additionalContext\": context\r\n            }\r\n        }\r\n\r\n        print(json.dumps(output))\r\n        sys.exit(0)\r\n\r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully\r\n        sys.exit(0)\r\n    except Exception:\r\n        # Handle any other errors gracefully\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == '__main__':\r\n    main()\r\n",
        "size": 10980
      },
      {
        "name": "stop.py",
        "path": "hooks/stop.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\nimport argparse\r\nimport json\r\nimport os\r\nimport sys\r\nimport random\r\nimport subprocess\r\nfrom pathlib import Path\r\nfrom datetime import datetime\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\ndef get_completion_messages():\r\n    \"\"\"Return list of friendly completion messages.\"\"\"\r\n    return [\r\n        \"Work complete!\",\r\n        \"All done!\",\r\n        \"Task finished!\",\r\n        \"Job complete!\",\r\n        \"Ready for next task!\"\r\n    ]\r\n\r\n\r\ndef get_tts_script_path():\r\n    \"\"\"\r\n    Determine which TTS script to use based on available API keys.\r\n    Priority order: ElevenLabs > OpenAI > pyttsx3\r\n    \"\"\"\r\n    # Get current script directory and construct utils/tts path\r\n    script_dir = Path(__file__).parent\r\n    tts_dir = script_dir / \"utils\" / \"tts\"\r\n    \r\n    # Check for ElevenLabs API key (highest priority)\r\n    if os.getenv('ELEVENLABS_API_KEY'):\r\n        elevenlabs_script = tts_dir / \"elevenlabs_tts.py\"\r\n        if elevenlabs_script.exists():\r\n            return str(elevenlabs_script)\r\n    \r\n    # Check for OpenAI API key (second priority)\r\n    if os.getenv('OPENAI_API_KEY'):\r\n        openai_script = tts_dir / \"openai_tts.py\"\r\n        if openai_script.exists():\r\n            return str(openai_script)\r\n    \r\n    # Fall back to pyttsx3 (no API key required)\r\n    pyttsx3_script = tts_dir / \"pyttsx3_tts.py\"\r\n    if pyttsx3_script.exists():\r\n        return str(pyttsx3_script)\r\n    \r\n    return None\r\n\r\n\r\ndef get_llm_completion_message():\r\n    \"\"\"\r\n    Generate completion message using available LLM services.\r\n    Priority order: OpenAI > Anthropic > Ollama > fallback to random message\r\n    \r\n    Returns:\r\n        str: Generated or fallback completion message\r\n    \"\"\"\r\n    # Get current script directory and construct utils/llm path\r\n    script_dir = Path(__file__).parent\r\n    llm_dir = script_dir / \"utils\" / \"llm\"\r\n    \r\n    # Try OpenAI first (highest priority)\r\n    if os.getenv('OPENAI_API_KEY'):\r\n        oai_script = llm_dir / \"oai.py\"\r\n        if oai_script.exists():\r\n            try:\r\n                result = subprocess.run([\r\n                    \"uv\", \"run\", str(oai_script), \"--completion\"\r\n                ], \r\n                capture_output=True,\r\n                text=True,\r\n                timeout=10\r\n                )\r\n                if result.returncode == 0 and result.stdout.strip():\r\n                    return result.stdout.strip()\r\n            except (subprocess.TimeoutExpired, subprocess.SubprocessError):\r\n                pass\r\n    \r\n    # Try Anthropic second\r\n    if os.getenv('ANTHROPIC_API_KEY'):\r\n        anth_script = llm_dir / \"anth.py\"\r\n        if anth_script.exists():\r\n            try:\r\n                result = subprocess.run([\r\n                    \"uv\", \"run\", str(anth_script), \"--completion\"\r\n                ], \r\n                capture_output=True,\r\n                text=True,\r\n                timeout=10\r\n                )\r\n                if result.returncode == 0 and result.stdout.strip():\r\n                    return result.stdout.strip()\r\n            except (subprocess.TimeoutExpired, subprocess.SubprocessError):\r\n                pass\r\n    \r\n    # Try Ollama third (local LLM)\r\n    ollama_script = llm_dir / \"ollama.py\"\r\n    if ollama_script.exists():\r\n        try:\r\n            result = subprocess.run([\r\n                \"uv\", \"run\", str(ollama_script), \"--completion\"\r\n            ], \r\n            capture_output=True,\r\n            text=True,\r\n            timeout=10\r\n            )\r\n            if result.returncode == 0 and result.stdout.strip():\r\n                return result.stdout.strip()\r\n        except (subprocess.TimeoutExpired, subprocess.SubprocessError):\r\n            pass\r\n    \r\n    # Fallback to random predefined message\r\n    messages = get_completion_messages()\r\n    return random.choice(messages)\r\n\r\ndef announce_completion():\r\n    \"\"\"Announce completion using the best available TTS service.\"\"\"\r\n    try:\r\n        tts_script = get_tts_script_path()\r\n        if not tts_script:\r\n            return  # No TTS scripts available\r\n        \r\n        # Get completion message (LLM-generated or fallback)\r\n        completion_message = get_llm_completion_message()\r\n        \r\n        # Call the TTS script with the completion message\r\n        subprocess.run([\r\n            \"uv\", \"run\", tts_script, completion_message\r\n        ], \r\n        capture_output=True,  # Suppress output\r\n        timeout=10  # 10-second timeout\r\n        )\r\n        \r\n    except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):\r\n        # Fail silently if TTS encounters issues\r\n        pass\r\n    except Exception:\r\n        # Fail silently for any other errors\r\n        pass\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Parse command line arguments\r\n        parser = argparse.ArgumentParser()\r\n        parser.add_argument('--chat', action='store_true', help='Copy transcript to chat.json')\r\n        parser.add_argument('--notify', action='store_true', help='Enable TTS completion announcement')\r\n        args = parser.parse_args()\r\n        \r\n        # Read JSON input from stdin\r\n        input_data = json.load(sys.stdin)\r\n\r\n        # Extract required fields\r\n        session_id = input_data.get(\"session_id\", \"\")\r\n        stop_hook_active = input_data.get(\"stop_hook_active\", False)\r\n\r\n        # Ensure log directory exists\r\n        log_dir = os.path.join(os.getcwd(), \"logs\")\r\n        os.makedirs(log_dir, exist_ok=True)\r\n        log_path = os.path.join(log_dir, \"stop.json\")\r\n\r\n        # Read existing log data or initialize empty list\r\n        if os.path.exists(log_path):\r\n            with open(log_path, 'r') as f:\r\n                try:\r\n                    log_data = json.load(f)\r\n                except (json.JSONDecodeError, ValueError):\r\n                    log_data = []\r\n        else:\r\n            log_data = []\r\n        \r\n        # Append new data\r\n        log_data.append(input_data)\r\n        \r\n        # Write back to file with formatting\r\n        with open(log_path, 'w') as f:\r\n            json.dump(log_data, f, indent=2)\r\n        \r\n        # Handle --chat switch\r\n        if args.chat and 'transcript_path' in input_data:\r\n            transcript_path = input_data['transcript_path']\r\n            if os.path.exists(transcript_path):\r\n                # Read .jsonl file and convert to JSON array\r\n                chat_data = []\r\n                try:\r\n                    with open(transcript_path, 'r') as f:\r\n                        for line in f:\r\n                            line = line.strip()\r\n                            if line:\r\n                                try:\r\n                                    chat_data.append(json.loads(line))\r\n                                except json.JSONDecodeError:\r\n                                    pass  # Skip invalid lines\r\n                    \r\n                    # Write to logs/chat.json\r\n                    chat_file = os.path.join(log_dir, 'chat.json')\r\n                    with open(chat_file, 'w') as f:\r\n                        json.dump(chat_data, f, indent=2)\r\n                except Exception:\r\n                    pass  # Fail silently\r\n\r\n        # Announce completion via TTS (only if --notify flag is set)\r\n        if args.notify:\r\n            announce_completion()\r\n\r\n        sys.exit(0)\r\n\r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully\r\n        sys.exit(0)\r\n    except Exception:\r\n        # Handle any other errors gracefully\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    main()\r\n",
        "size": 7697
      },
      {
        "name": "subagent_start.py",
        "path": "hooks/subagent_start.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\nimport argparse\r\nimport json\r\nimport os\r\nimport sys\r\nimport subprocess\r\nfrom pathlib import Path\r\nfrom datetime import datetime\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\ndef debug_log(message: str) -> None:\r\n    \"\"\"Write debug message to logs/subagent_debug.log\"\"\"\r\n    try:\r\n        log_dir = os.path.join(os.getcwd(), \"logs\")\r\n        os.makedirs(log_dir, exist_ok=True)\r\n        debug_path = os.path.join(log_dir, \"subagent_debug.log\")\r\n        timestamp = datetime.now().isoformat()\r\n        with open(debug_path, 'a') as f:\r\n            f.write(f\"[{timestamp}] [START] {message}\\n\")\r\n    except Exception:\r\n        pass\r\n\r\n\r\ndef get_tts_script_path() -> str | None:\r\n    \"\"\"\r\n    Determine which TTS script to use based on available API keys.\r\n    Priority order: ElevenLabs > OpenAI > pyttsx3\r\n    \"\"\"\r\n    # Get current script directory and construct utils/tts path\r\n    script_dir = Path(__file__).parent\r\n    tts_dir = script_dir / \"utils\" / \"tts\"\r\n\r\n    # Check for ElevenLabs API key (highest priority)\r\n    if os.getenv('ELEVENLABS_API_KEY'):\r\n        elevenlabs_script = tts_dir / \"elevenlabs_tts.py\"\r\n        if elevenlabs_script.exists():\r\n            return str(elevenlabs_script)\r\n\r\n    # Check for OpenAI API key (second priority)\r\n    if os.getenv('OPENAI_API_KEY'):\r\n        openai_script = tts_dir / \"openai_tts.py\"\r\n        if openai_script.exists():\r\n            return str(openai_script)\r\n\r\n    # Fall back to pyttsx3 (no API key required)\r\n    pyttsx3_script = tts_dir / \"pyttsx3_tts.py\"\r\n    if pyttsx3_script.exists():\r\n        return str(pyttsx3_script)\r\n\r\n    return None\r\n\r\n\r\ndef announce_subagent_start(message: str = \"Subagent Started\") -> None:\r\n    \"\"\"Announce subagent start using the best available TTS service.\r\n\r\n    Args:\r\n        message: The message to announce via TTS\r\n    \"\"\"\r\n    try:\r\n        tts_script = get_tts_script_path()\r\n        if not tts_script:\r\n            return  # No TTS scripts available\r\n\r\n        # Call the TTS script with the provided message\r\n        subprocess.run([\r\n            \"uv\", \"run\", tts_script, message\r\n        ],\r\n        capture_output=True,  # Suppress output\r\n        timeout=10  # 10-second timeout\r\n        )\r\n\r\n    except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):\r\n        # Fail silently if TTS encounters issues\r\n        pass\r\n    except Exception:\r\n        # Fail silently for any other errors\r\n        pass\r\n\r\n\r\ndef main() -> None:\r\n    try:\r\n        # Parse command line arguments\r\n        parser = argparse.ArgumentParser(\r\n            description=\"SubagentStart hook - logs and optionally announces subagent spawn events\"\r\n        )\r\n        parser.add_argument('--notify', action='store_true',\r\n                            help='Enable TTS announcement when subagent starts')\r\n        args = parser.parse_args()\r\n\r\n        # Read JSON input from stdin\r\n        input_data = json.load(sys.stdin)\r\n\r\n        # Extract fields for logging/announcement (only extract what we use)\r\n        agent_id = input_data.get(\"agent_id\", \"unknown\")\r\n        agent_type = input_data.get(\"agent_type\", \"unknown\")\r\n\r\n        # Add timestamp to input data for logging\r\n        input_data[\"logged_at\"] = datetime.now().isoformat()\r\n\r\n        # Ensure log directory exists\r\n        log_dir = os.path.join(os.getcwd(), \"logs\")\r\n        os.makedirs(log_dir, exist_ok=True)\r\n        log_path = os.path.join(log_dir, \"subagent_start.json\")\r\n\r\n        # Read existing log data or initialize empty list\r\n        if os.path.exists(log_path):\r\n            with open(log_path, 'r') as f:\r\n                try:\r\n                    log_data = json.load(f)\r\n                except (json.JSONDecodeError, ValueError):\r\n                    log_data = []\r\n        else:\r\n            log_data = []\r\n\r\n        # Append new data\r\n        log_data.append(input_data)\r\n\r\n        # Write back to file with formatting\r\n        with open(log_path, 'w') as f:\r\n            json.dump(log_data, f, indent=2)\r\n\r\n        debug_log(f\"Logged SubagentStart: agent_id={agent_id}, agent_type={agent_type}\")\r\n\r\n        # Announce subagent start via TTS (only if --notify flag is set)\r\n        if args.notify:\r\n            debug_log(f\"=== SubagentStart for agent: {agent_id} ===\")\r\n            debug_log(f\"agent_type: {agent_type}\")\r\n\r\n            # Create announcement message\r\n            if agent_type and agent_type != \"unknown\":\r\n                announcement = f\"{agent_type} agent started\"\r\n            else:\r\n                announcement = \"Subagent started\"\r\n\r\n            debug_log(f\"Announcing: {announcement}\")\r\n            announce_subagent_start(announcement)\r\n\r\n        sys.exit(0)\r\n\r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully\r\n        sys.exit(0)\r\n    except Exception:\r\n        # Handle any other errors gracefully\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    main()\r\n",
        "size": 5124
      },
      {
        "name": "subagent_stop.py",
        "path": "hooks/subagent_stop.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n#     \"anthropic\",\r\n# ]\r\n# ///\r\n\r\nimport argparse\r\nimport json\r\nimport os\r\nimport sys\r\nimport subprocess\r\nfrom pathlib import Path\r\nfrom typing import Optional\r\nfrom datetime import datetime\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\ndef debug_log(message: str) -> None:\r\n    \"\"\"Write debug message to logs/subagent_debug.log\"\"\"\r\n    try:\r\n        log_dir = os.path.join(os.getcwd(), \"logs\")\r\n        os.makedirs(log_dir, exist_ok=True)\r\n        debug_path = os.path.join(log_dir, \"subagent_debug.log\")\r\n        timestamp = datetime.now().isoformat()\r\n        with open(debug_path, 'a') as f:\r\n            f.write(f\"[{timestamp}] {message}\\n\")\r\n    except Exception:\r\n        pass\r\n\r\n# Add hooks directory to path for local imports\r\nsys.path.insert(0, str(Path(__file__).parent))\r\n\r\ntry:\r\n    from utils.tts.tts_queue import acquire_tts_lock, release_tts_lock, cleanup_stale_locks\r\nexcept ImportError:\r\n    # Fallback if imports fail - provide no-op functions\r\n    def acquire_tts_lock(agent_id: str, timeout: int = 30) -> bool:\r\n        return True\r\n    def release_tts_lock(agent_id: str) -> None:\r\n        pass\r\n    def cleanup_stale_locks(max_age_seconds: int = 60) -> None:\r\n        pass\r\n\r\ntry:\r\n    from utils.llm.task_summarizer import summarize_subagent_task\r\nexcept ImportError:\r\n    # Fallback if imports fail\r\n    def summarize_subagent_task(task_description: str, agent_name: Optional[str] = None) -> str:\r\n        return \"Subagent Complete\"\r\n\r\n\r\ndef get_tts_script_path() -> Optional[str]:\r\n    \"\"\"\r\n    Determine which TTS script to use based on available API keys.\r\n    Priority order: ElevenLabs > OpenAI > pyttsx3\r\n    \"\"\"\r\n    # Get current script directory and construct utils/tts path\r\n    script_dir = Path(__file__).parent\r\n    tts_dir = script_dir / \"utils\" / \"tts\"\r\n\r\n    # Check for ElevenLabs API key (highest priority)\r\n    if os.getenv('ELEVENLABS_API_KEY'):\r\n        elevenlabs_script = tts_dir / \"elevenlabs_tts.py\"\r\n        if elevenlabs_script.exists():\r\n            return str(elevenlabs_script)\r\n\r\n    # Check for OpenAI API key (second priority)\r\n    if os.getenv('OPENAI_API_KEY'):\r\n        openai_script = tts_dir / \"openai_tts.py\"\r\n        if openai_script.exists():\r\n            return str(openai_script)\r\n\r\n    # Fall back to pyttsx3 (no API key required)\r\n    pyttsx3_script = tts_dir / \"pyttsx3_tts.py\"\r\n    if pyttsx3_script.exists():\r\n        return str(pyttsx3_script)\r\n\r\n    return None\r\n\r\n\r\ndef extract_task_context(input_data: dict) -> str:\r\n    \"\"\"\r\n    Extract task context from the subagent input data.\r\n\r\n    Looks for the agent transcript path and reads the initial task/prompt\r\n    from the JSONL transcript file.\r\n\r\n    Args:\r\n        input_data: The input data dictionary from stdin\r\n\r\n    Returns:\r\n        A brief description of what the subagent was doing\r\n    \"\"\"\r\n    # Try to get agent transcript path\r\n    transcript_path = input_data.get(\"agent_transcript_path\")\r\n    if not transcript_path:\r\n        # Fallback to regular transcript_path\r\n        transcript_path = input_data.get(\"transcript_path\")\r\n\r\n    if not transcript_path or not os.path.exists(transcript_path):\r\n        return \"completed a task\"\r\n\r\n    try:\r\n        # Read the JSONL transcript file\r\n        with open(transcript_path, 'r') as f:\r\n            for line in f:\r\n                line = line.strip()\r\n                if not line:\r\n                    continue\r\n                try:\r\n                    entry = json.loads(line)\r\n\r\n                    # Look for user messages or initial prompts\r\n                    entry_type = entry.get(\"type\", \"\")\r\n\r\n                    if entry_type == \"user\":\r\n                        # The content is nested in message.content\r\n                        message = entry.get(\"message\", {})\r\n                        content = message.get(\"content\", \"\") if isinstance(message, dict) else \"\"\r\n\r\n                        # Fallback to direct content field\r\n                        if not content:\r\n                            content = entry.get(\"content\", \"\")\r\n\r\n                        if isinstance(content, str) and content:\r\n                            # Truncate if too long\r\n                            if len(content) > 200:\r\n                                return content[:200] + \"...\"\r\n                            return content\r\n                        elif isinstance(content, list):\r\n                            # Handle content blocks\r\n                            for block in content:\r\n                                if isinstance(block, dict) and block.get(\"type\") == \"text\":\r\n                                    text = block.get(\"text\", \"\")\r\n                                    if text:\r\n                                        if len(text) > 200:\r\n                                            return text[:200] + \"...\"\r\n                                        return text\r\n\r\n                    # Also check for prompt field\r\n                    prompt = entry.get(\"prompt\", \"\")\r\n                    if prompt:\r\n                        if len(prompt) > 200:\r\n                            return prompt[:200] + \"...\"\r\n                        return prompt\r\n\r\n                except json.JSONDecodeError:\r\n                    continue\r\n\r\n    except (OSError, IOError):\r\n        pass\r\n\r\n    return \"completed a task\"\r\n\r\n\r\ndef announce_subagent_completion(message: str = \"Subagent Complete\") -> None:\r\n    \"\"\"Announce subagent completion using the best available TTS service.\r\n\r\n    Args:\r\n        message: The message to announce via TTS\r\n    \"\"\"\r\n    try:\r\n        tts_script = get_tts_script_path()\r\n        if not tts_script:\r\n            return  # No TTS scripts available\r\n\r\n        # Call the TTS script with the provided message\r\n        subprocess.run([\r\n            \"uv\", \"run\", tts_script, message\r\n        ],\r\n        capture_output=True,  # Suppress output\r\n        timeout=10  # 10-second timeout\r\n        )\r\n\r\n    except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):\r\n        # Fail silently if TTS encounters issues\r\n        pass\r\n    except Exception:\r\n        # Fail silently for any other errors\r\n        pass\r\n\r\n\r\ndef main() -> None:\r\n    try:\r\n        # Parse command line arguments\r\n        parser = argparse.ArgumentParser()\r\n        parser.add_argument('--chat', action='store_true', help='Copy transcript to chat.json')\r\n        parser.add_argument('--notify', action='store_true', help='Enable TTS completion announcement')\r\n        parser.add_argument('--summarize', action='store_true', default=True,\r\n                            help='Generate AI summary of subagent task (default: on when --notify is used)')\r\n        parser.add_argument('--no-summarize', dest='summarize', action='store_false',\r\n                            help='Disable AI summary, use generic message')\r\n        args = parser.parse_args()\r\n\r\n        # Read JSON input from stdin\r\n        input_data = json.load(sys.stdin)\r\n\r\n        # Extract required fields (used for logging context)\r\n        _ = input_data.get(\"session_id\", \"\")\r\n        _ = input_data.get(\"stop_hook_active\", False)\r\n\r\n        # Ensure log directory exists\r\n        log_dir = os.path.join(os.getcwd(), \"logs\")\r\n        os.makedirs(log_dir, exist_ok=True)\r\n        log_path = os.path.join(log_dir, \"subagent_stop.json\")\r\n\r\n        # Read existing log data or initialize empty list\r\n        if os.path.exists(log_path):\r\n            with open(log_path, 'r') as f:\r\n                try:\r\n                    log_data = json.load(f)\r\n                except (json.JSONDecodeError, ValueError):\r\n                    log_data = []\r\n        else:\r\n            log_data = []\r\n\r\n        # Append new data\r\n        log_data.append(input_data)\r\n\r\n        # Write back to file with formatting\r\n        with open(log_path, 'w') as f:\r\n            json.dump(log_data, f, indent=2)\r\n\r\n        # Handle --chat switch (same as stop.py)\r\n        if args.chat and 'transcript_path' in input_data:\r\n            transcript_path = input_data['transcript_path']\r\n            if os.path.exists(transcript_path):\r\n                # Read .jsonl file and convert to JSON array\r\n                chat_data = []\r\n                try:\r\n                    with open(transcript_path, 'r') as f:\r\n                        for line in f:\r\n                            line = line.strip()\r\n                            if line:\r\n                                try:\r\n                                    chat_data.append(json.loads(line))\r\n                                except json.JSONDecodeError:\r\n                                    pass  # Skip invalid lines\r\n\r\n                    # Write to logs/chat.json\r\n                    chat_file = os.path.join(log_dir, 'chat.json')\r\n                    with open(chat_file, 'w') as f:\r\n                        json.dump(chat_data, f, indent=2)\r\n                except Exception:\r\n                    pass  # Fail silently\r\n\r\n        # Announce subagent completion via TTS (only if --notify flag is set)\r\n        if args.notify:\r\n            agent_id = input_data.get(\"agent_id\", \"unknown\")\r\n            debug_log(f\"=== SubagentStop for agent: {agent_id} ===\")\r\n            debug_log(f\"agent_transcript_path: {input_data.get('agent_transcript_path', 'NOT FOUND')}\")\r\n            debug_log(f\"ANTHROPIC_API_KEY present: {bool(os.getenv('ANTHROPIC_API_KEY'))}\")\r\n\r\n            # Clean up any stale locks first\r\n            cleanup_stale_locks(max_age_seconds=60)\r\n\r\n            # Generate summary message\r\n            if args.summarize:\r\n                task_context = extract_task_context(input_data)\r\n                debug_log(f\"Extracted task_context: {task_context[:100]}...\")\r\n                summary_message = summarize_subagent_task(task_context, agent_name=agent_id)\r\n                debug_log(f\"Generated summary_message: {summary_message}\")\r\n            else:\r\n                summary_message = \"Subagent Complete\"\r\n                debug_log(\"Summarize disabled, using default message\")\r\n\r\n            # Acquire lock before speaking (blocks until available or timeout)\r\n            if acquire_tts_lock(agent_id, timeout=30):\r\n                try:\r\n                    debug_log(f\"Lock acquired, announcing: {summary_message}\")\r\n                    announce_subagent_completion(summary_message)\r\n                finally:\r\n                    release_tts_lock(agent_id)\r\n                    debug_log(\"Lock released\")\r\n            else:\r\n                # Timeout - still announce but log warning\r\n                debug_log(f\"Lock timeout, announcing anyway: {summary_message}\")\r\n                announce_subagent_completion(summary_message)\r\n\r\n        sys.exit(0)\r\n\r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully\r\n        sys.exit(0)\r\n    except Exception:\r\n        # Handle any other errors gracefully\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    main()\r\n",
        "size": 11102
      },
      {
        "name": "user_prompt_submit.py",
        "path": "hooks/user_prompt_submit.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\nimport argparse\r\nimport json\r\nimport os\r\nimport sys\r\nfrom pathlib import Path\r\nfrom datetime import datetime\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\ndef log_user_prompt(session_id, input_data):\r\n    \"\"\"Log user prompt to logs directory.\"\"\"\r\n    # Ensure logs directory exists\r\n    log_dir = Path(\"logs\")\r\n    log_dir.mkdir(parents=True, exist_ok=True)\r\n    log_file = log_dir / 'user_prompt_submit.json'\r\n    \r\n    # Read existing log data or initialize empty list\r\n    if log_file.exists():\r\n        with open(log_file, 'r') as f:\r\n            try:\r\n                log_data = json.load(f)\r\n            except (json.JSONDecodeError, ValueError):\r\n                log_data = []\r\n    else:\r\n        log_data = []\r\n    \r\n    # Append the entire input data\r\n    log_data.append(input_data)\r\n    \r\n    # Write back to file with formatting\r\n    with open(log_file, 'w') as f:\r\n        json.dump(log_data, f, indent=2)\r\n\r\n\r\n# Legacy function removed - now handled by manage_session_data\r\n\r\n\r\ndef manage_session_data(session_id, prompt, name_agent=False):\r\n    \"\"\"Manage session data in the new JSON structure.\"\"\"\r\n    import subprocess\r\n    \r\n    # Ensure sessions directory exists\r\n    sessions_dir = Path(\".claude/data/sessions\")\r\n    sessions_dir.mkdir(parents=True, exist_ok=True)\r\n    \r\n    # Load or create session file\r\n    session_file = sessions_dir / f\"{session_id}.json\"\r\n    \r\n    if session_file.exists():\r\n        try:\r\n            with open(session_file, 'r') as f:\r\n                session_data = json.load(f)\r\n        except (json.JSONDecodeError, ValueError):\r\n            session_data = {\"session_id\": session_id, \"prompts\": []}\r\n    else:\r\n        session_data = {\"session_id\": session_id, \"prompts\": []}\r\n    \r\n    # Add the new prompt\r\n    session_data[\"prompts\"].append(prompt)\r\n    \r\n    # Generate agent name if requested and not already present\r\n    if name_agent and \"agent_name\" not in session_data:\r\n        # Try Ollama first (preferred)\r\n        try:\r\n            result = subprocess.run(\r\n                [\"uv\", \"run\", \".claude/hooks/utils/llm/ollama.py\", \"--agent-name\"],\r\n                capture_output=True,\r\n                text=True,\r\n                timeout=5  # Shorter timeout for local Ollama\r\n            )\r\n            \r\n            if result.returncode == 0 and result.stdout.strip():\r\n                agent_name = result.stdout.strip()\r\n                # Check if it's a valid name (not an error message)\r\n                if len(agent_name.split()) == 1 and agent_name.isalnum():\r\n                    session_data[\"agent_name\"] = agent_name\r\n                else:\r\n                    raise Exception(\"Invalid name from Ollama\")\r\n        except Exception:\r\n            # Fall back to Anthropic if Ollama fails\r\n            try:\r\n                result = subprocess.run(\r\n                    [\"uv\", \"run\", \".claude/hooks/utils/llm/anth.py\", \"--agent-name\"],\r\n                    capture_output=True,\r\n                    text=True,\r\n                    timeout=10\r\n                )\r\n                \r\n                if result.returncode == 0 and result.stdout.strip():\r\n                    agent_name = result.stdout.strip()\r\n                    # Validate the name\r\n                    if len(agent_name.split()) == 1 and agent_name.isalnum():\r\n                        session_data[\"agent_name\"] = agent_name\r\n            except Exception:\r\n                # If both fail, don't block the prompt\r\n                pass\r\n    \r\n    # Save the updated session data\r\n    try:\r\n        with open(session_file, 'w') as f:\r\n            json.dump(session_data, f, indent=2)\r\n    except Exception:\r\n        # Silently fail if we can't write the file\r\n        pass\r\n\r\n\r\ndef validate_prompt(prompt):\r\n    \"\"\"\r\n    Validate the user prompt for security or policy violations.\r\n    Returns tuple (is_valid, reason).\r\n    \"\"\"\r\n    # Example validation rules (customize as needed)\r\n    blocked_patterns = [\r\n        # Add any patterns you want to block\r\n        # Example: ('rm -rf /', 'Dangerous command detected'),\r\n    ]\r\n    \r\n    prompt_lower = prompt.lower()\r\n    \r\n    for pattern, reason in blocked_patterns:\r\n        if pattern.lower() in prompt_lower:\r\n            return False, reason\r\n    \r\n    return True, None\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Parse command line arguments\r\n        parser = argparse.ArgumentParser()\r\n        parser.add_argument('--validate', action='store_true', \r\n                          help='Enable prompt validation')\r\n        parser.add_argument('--log-only', action='store_true',\r\n                          help='Only log prompts, no validation or blocking')\r\n        parser.add_argument('--store-last-prompt', action='store_true',\r\n                          help='Store the last prompt for status line display')\r\n        parser.add_argument('--name-agent', action='store_true',\r\n                          help='Generate an agent name for the session')\r\n        args = parser.parse_args()\r\n        \r\n        # Read JSON input from stdin\r\n        input_data = json.loads(sys.stdin.read())\r\n        \r\n        # Extract session_id and prompt\r\n        session_id = input_data.get('session_id', 'unknown')\r\n        prompt = input_data.get('prompt', '')\r\n        \r\n        # Log the user prompt\r\n        log_user_prompt(session_id, input_data)\r\n        \r\n        # Manage session data with JSON structure\r\n        if args.store_last_prompt or args.name_agent:\r\n            manage_session_data(session_id, prompt, name_agent=args.name_agent)\r\n        \r\n        # Validate prompt if requested and not in log-only mode\r\n        if args.validate and not args.log_only:\r\n            is_valid, reason = validate_prompt(prompt)\r\n            if not is_valid:\r\n                # Exit code 2 blocks the prompt with error message\r\n                print(f\"Prompt blocked: {reason}\", file=sys.stderr)\r\n                sys.exit(2)\r\n        \r\n        # Add context information (optional)\r\n        # You can print additional context that will be added to the prompt\r\n        # Example: print(f\"Current time: {datetime.now()}\")\r\n        \r\n        # Success - prompt will be processed\r\n        sys.exit(0)\r\n        \r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully\r\n        sys.exit(0)\r\n    except Exception:\r\n        # Handle any other errors gracefully\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == '__main__':\r\n    main()",
        "size": 6615
      },
      {
        "name": "status_line.py",
        "path": "status_lines/status_line.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\nimport json\r\nimport os\r\nimport sys\r\nimport subprocess\r\nfrom pathlib import Path\r\nfrom datetime import datetime\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\ndef log_status_line(input_data, status_line_output):\r\n    \"\"\"Log status line event to logs directory.\"\"\"\r\n    # Ensure logs directory exists\r\n    log_dir = Path(\"logs\")\r\n    log_dir.mkdir(parents=True, exist_ok=True)\r\n    log_file = log_dir / 'status_line.json'\r\n    \r\n    # Read existing log data or initialize empty list\r\n    if log_file.exists():\r\n        with open(log_file, 'r') as f:\r\n            try:\r\n                log_data = json.load(f)\r\n            except (json.JSONDecodeError, ValueError):\r\n                log_data = []\r\n    else:\r\n        log_data = []\r\n    \r\n    # Create log entry with input data and generated output\r\n    log_entry = {\r\n        \"timestamp\": datetime.now().isoformat(),\r\n        \"input_data\": input_data,\r\n        \"status_line_output\": status_line_output\r\n    }\r\n    \r\n    # Append the log entry\r\n    log_data.append(log_entry)\r\n    \r\n    # Write back to file with formatting\r\n    with open(log_file, 'w') as f:\r\n        json.dump(log_data, f, indent=2)\r\n\r\n\r\ndef get_git_branch():\r\n    \"\"\"Get current git branch if in a git repository.\"\"\"\r\n    try:\r\n        result = subprocess.run(\r\n            ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],\r\n            capture_output=True,\r\n            text=True,\r\n            timeout=2\r\n        )\r\n        if result.returncode == 0:\r\n            return result.stdout.strip()\r\n    except Exception:\r\n        pass\r\n    return None\r\n\r\n\r\ndef get_git_status():\r\n    \"\"\"Get git status indicators.\"\"\"\r\n    try:\r\n        # Check if there are uncommitted changes\r\n        result = subprocess.run(\r\n            ['git', 'status', '--porcelain'],\r\n            capture_output=True,\r\n            text=True,\r\n            timeout=2\r\n        )\r\n        if result.returncode == 0:\r\n            changes = result.stdout.strip()\r\n            if changes:\r\n                lines = changes.split('\\n')\r\n                return f\"±{len(lines)}\"\r\n    except Exception:\r\n        pass\r\n    return \"\"\r\n\r\n\r\ndef generate_status_line(input_data):\r\n    \"\"\"Generate the status line based on input data.\"\"\"\r\n    parts = []\r\n    \r\n    # Model display name\r\n    model_info = input_data.get('model', {})\r\n    model_name = model_info.get('display_name', 'Claude')\r\n    parts.append(f\"\\033[36m[{model_name}]\\033[0m\")  # Cyan color\r\n    \r\n    # Current directory\r\n    workspace = input_data.get('workspace', {})\r\n    current_dir = workspace.get('current_dir', '')\r\n    if current_dir:\r\n        dir_name = os.path.basename(current_dir)\r\n        parts.append(f\"\\033[34m📁 {dir_name}\\033[0m\")  # Blue color\r\n    \r\n    # Git branch and status\r\n    git_branch = get_git_branch()\r\n    if git_branch:\r\n        git_status = get_git_status()\r\n        git_info = f\"🌿 {git_branch}\"\r\n        if git_status:\r\n            git_info += f\" {git_status}\"\r\n        parts.append(f\"\\033[32m{git_info}\\033[0m\")  # Green color\r\n    \r\n    # Version info (optional, smaller)\r\n    version = input_data.get('version', '')\r\n    if version:\r\n        parts.append(f\"\\033[90mv{version}\\033[0m\")  # Gray color\r\n    \r\n    return \" | \".join(parts)\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Read JSON input from stdin\r\n        input_data = json.loads(sys.stdin.read())\r\n        \r\n        # Generate status line\r\n        status_line = generate_status_line(input_data)\r\n        \r\n        # Log the status line event\r\n        log_status_line(input_data, status_line)\r\n        \r\n        # Output the status line (first line of stdout becomes the status line)\r\n        print(status_line)\r\n        \r\n        # Success\r\n        sys.exit(0)\r\n        \r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully - output basic status\r\n        print(\"\\033[31m[Claude] 📁 Unknown\\033[0m\")\r\n        sys.exit(0)\r\n    except Exception:\r\n        # Handle any other errors gracefully - output basic status\r\n        print(\"\\033[31m[Claude] 📁 Error\\033[0m\")\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == '__main__':\r\n    main()",
        "size": 4298
      },
      {
        "name": "status_line_v2.py",
        "path": "status_lines/status_line_v2.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\nimport json\r\nimport os\r\nimport sys\r\nfrom pathlib import Path\r\nfrom datetime import datetime\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\ndef log_status_line(input_data, status_line_output, error_message=None):\r\n    \"\"\"Log status line event to logs directory.\"\"\"\r\n    # Ensure logs directory exists\r\n    log_dir = Path(\"logs\")\r\n    log_dir.mkdir(parents=True, exist_ok=True)\r\n    log_file = log_dir / 'status_line.json'\r\n    \r\n    # Read existing log data or initialize empty list\r\n    if log_file.exists():\r\n        with open(log_file, 'r') as f:\r\n            try:\r\n                log_data = json.load(f)\r\n            except (json.JSONDecodeError, ValueError):\r\n                log_data = []\r\n    else:\r\n        log_data = []\r\n    \r\n    # Create log entry with input data and generated output\r\n    log_entry = {\r\n        \"timestamp\": datetime.now().isoformat(),\r\n        \"version\": \"v2\",\r\n        \"input_data\": input_data,\r\n        \"status_line_output\": status_line_output,\r\n    }\r\n    \r\n    if error_message:\r\n        log_entry[\"error\"] = error_message\r\n    \r\n    # Append the log entry\r\n    log_data.append(log_entry)\r\n    \r\n    # Write back to file with formatting\r\n    with open(log_file, 'w') as f:\r\n        json.dump(log_data, f, indent=2)\r\n\r\n\r\ndef get_last_prompt(session_id):\r\n    \"\"\"Get the last prompt for the current session.\"\"\"\r\n    # Use the JSON structure\r\n    session_file = Path(f\".claude/data/sessions/{session_id}.json\")\r\n    \r\n    if not session_file.exists():\r\n        return None, f\"Session file {session_file} does not exist\"\r\n    \r\n    try:\r\n        with open(session_file, 'r') as f:\r\n            session_data = json.load(f)\r\n            prompts = session_data.get(\"prompts\", [])\r\n            if prompts:\r\n                return prompts[-1], None\r\n            return None, \"No prompts in session\"\r\n    except Exception as e:\r\n        return None, f\"Error reading session file: {str(e)}\"\r\n\r\n\r\ndef generate_status_line(input_data):\r\n    \"\"\"Generate the status line showing the last prompt.\"\"\"\r\n    # Extract session ID from input data\r\n    session_id = input_data.get('session_id', 'unknown')\r\n    \r\n    # Get model name for prefix\r\n    model_info = input_data.get('model', {})\r\n    model_name = model_info.get('display_name', 'Claude')\r\n    \r\n    # Get the last prompt\r\n    prompt, error = get_last_prompt(session_id)\r\n    \r\n    if error:\r\n        # Log the error but show a default message\r\n        log_status_line(input_data, f\"[{model_name}] 💭 No recent prompt\", error)\r\n        return f\"\\033[36m[{model_name}]\\033[0m \\033[90m💭 No recent prompt\\033[0m\"\r\n    \r\n    # Format the prompt for status line\r\n    # Remove newlines and excessive whitespace\r\n    prompt = ' '.join(prompt.split())\r\n    \r\n    # Color coding based on prompt type\r\n    if prompt.startswith('/'):\r\n        # Command prompt - yellow\r\n        prompt_color = \"\\033[33m\"\r\n        icon = \"⚡\"\r\n    elif '?' in prompt:\r\n        # Question - blue\r\n        prompt_color = \"\\033[34m\"\r\n        icon = \"❓\"\r\n    elif any(word in prompt.lower() for word in ['create', 'write', 'add', 'implement', 'build']):\r\n        # Creation task - green\r\n        prompt_color = \"\\033[32m\"\r\n        icon = \"💡\"\r\n    elif any(word in prompt.lower() for word in ['fix', 'debug', 'error', 'issue']):\r\n        # Fix/debug task - red\r\n        prompt_color = \"\\033[31m\"\r\n        icon = \"🐛\"\r\n    elif any(word in prompt.lower() for word in ['refactor', 'improve', 'optimize']):\r\n        # Refactor task - magenta\r\n        prompt_color = \"\\033[35m\"\r\n        icon = \"♻️\"\r\n    else:\r\n        # Default - white\r\n        prompt_color = \"\\033[37m\"\r\n        icon = \"💬\"\r\n    \r\n    # Construct the status line\r\n    status_line = f\"\\033[36m[{model_name}]\\033[0m {icon} {prompt_color}{prompt}\\033[0m\"\r\n    \r\n    return status_line\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Read JSON input from stdin\r\n        input_data = json.loads(sys.stdin.read())\r\n        \r\n        # Generate status line\r\n        status_line = generate_status_line(input_data)\r\n        \r\n        # Log the status line event (without error since it's successful)\r\n        log_status_line(input_data, status_line)\r\n        \r\n        # Output the status line (first line of stdout becomes the status line)\r\n        print(status_line)\r\n        \r\n        # Success\r\n        sys.exit(0)\r\n        \r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully - output basic status\r\n        print(\"\\033[31m[Claude] 💭 JSON Error\\033[0m\")\r\n        sys.exit(0)\r\n    except Exception as e:\r\n        # Handle any other errors gracefully - output basic status\r\n        print(f\"\\033[31m[Claude] 💭 Error: {str(e)}\\033[0m\")\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == '__main__':\r\n    main()",
        "size": 4951
      },
      {
        "name": "status_line_v3.py",
        "path": "status_lines/status_line_v3.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\nimport json\r\nimport os\r\nimport sys\r\nfrom pathlib import Path\r\nfrom datetime import datetime\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\ndef log_status_line(input_data, status_line_output, error_message=None):\r\n    \"\"\"Log status line event to logs directory.\"\"\"\r\n    # Ensure logs directory exists\r\n    log_dir = Path(\"logs\")\r\n    log_dir.mkdir(parents=True, exist_ok=True)\r\n    log_file = log_dir / \"status_line.json\"\r\n\r\n    # Read existing log data or initialize empty list\r\n    if log_file.exists():\r\n        with open(log_file, \"r\") as f:\r\n            try:\r\n                log_data = json.load(f)\r\n            except (json.JSONDecodeError, ValueError):\r\n                log_data = []\r\n    else:\r\n        log_data = []\r\n\r\n    # Create log entry with input data and generated output\r\n    log_entry = {\r\n        \"timestamp\": datetime.now().isoformat(),\r\n        \"version\": \"v3\",\r\n        \"input_data\": input_data,\r\n        \"status_line_output\": status_line_output,\r\n    }\r\n\r\n    if error_message:\r\n        log_entry[\"error\"] = error_message\r\n\r\n    # Append the log entry\r\n    log_data.append(log_entry)\r\n\r\n    # Write back to file with formatting\r\n    with open(log_file, \"w\") as f:\r\n        json.dump(log_data, f, indent=2)\r\n\r\n\r\ndef get_session_data(session_id):\r\n    \"\"\"Get session data including agent name and prompts.\"\"\"\r\n    session_file = Path(f\".claude/data/sessions/{session_id}.json\")\r\n\r\n    if not session_file.exists():\r\n        return None, f\"Session file {session_file} does not exist\"\r\n\r\n    try:\r\n        with open(session_file, \"r\") as f:\r\n            session_data = json.load(f)\r\n            return session_data, None\r\n    except Exception as e:\r\n        return None, f\"Error reading session file: {str(e)}\"\r\n\r\n\r\ndef truncate_prompt(prompt, max_length=75):\r\n    \"\"\"Truncate prompt to specified length.\"\"\"\r\n    # Remove newlines and excessive whitespace\r\n    prompt = \" \".join(prompt.split())\r\n\r\n    if len(prompt) > max_length:\r\n        return prompt[: max_length - 3] + \"...\"\r\n    return prompt\r\n\r\n\r\ndef get_prompt_icon(prompt):\r\n    \"\"\"Get icon based on prompt type.\"\"\"\r\n    if prompt.startswith(\"/\"):\r\n        return \"⚡\"\r\n    elif \"?\" in prompt:\r\n        return \"❓\"\r\n    elif any(\r\n        word in prompt.lower()\r\n        for word in [\"create\", \"write\", \"add\", \"implement\", \"build\"]\r\n    ):\r\n        return \"💡\"\r\n    elif any(word in prompt.lower() for word in [\"fix\", \"debug\", \"error\", \"issue\"]):\r\n        return \"🐛\"\r\n    elif any(word in prompt.lower() for word in [\"refactor\", \"improve\", \"optimize\"]):\r\n        return \"♻️\"\r\n    else:\r\n        return \"💬\"\r\n\r\n\r\ndef generate_status_line(input_data):\r\n    \"\"\"Generate the status line with agent name and last 3 prompts.\"\"\"\r\n    # Extract session ID from input data\r\n    session_id = input_data.get(\"session_id\", \"unknown\")\r\n\r\n    # Get model name\r\n    model_info = input_data.get(\"model\", {})\r\n    model_name = model_info.get(\"display_name\", \"Claude\")\r\n\r\n    # Get session data\r\n    session_data, error = get_session_data(session_id)\r\n\r\n    if error:\r\n        # Log the error but show a default message\r\n        log_status_line(input_data, f\"[{model_name}] 💭 No session data\", error)\r\n        return f\"\\033[36m[{model_name}]\\033[0m \\033[90m💭 No session data\\033[0m\"\r\n\r\n    # Extract agent name and prompts\r\n    agent_name = session_data.get(\"agent_name\", \"Agent\")\r\n    prompts = session_data.get(\"prompts\", [])\r\n\r\n    # Build status line components\r\n    parts = []\r\n\r\n    # Agent name - Bright Green\r\n    parts.append(f\"\\033[91m[{agent_name}]\\033[0m\")\r\n\r\n    # Model name - Blue\r\n    parts.append(f\"\\033[34m[{model_name}]\\033[0m\")\r\n\r\n    # Last 3 prompts (most recent first)\r\n    if prompts:\r\n        # Current prompt - white/bright\r\n        current_prompt = prompts[-1]\r\n        icon = get_prompt_icon(current_prompt)\r\n        truncated = truncate_prompt(current_prompt, 75)\r\n        parts.append(f\"{icon} \\033[97m{truncated}\\033[0m\")\r\n\r\n        # Previous prompt - gray\r\n        if len(prompts) > 1:\r\n            prev_prompt = prompts[-2]\r\n            truncated = truncate_prompt(prev_prompt, 50)\r\n            parts.append(f\"\\033[90m{truncated}\\033[0m\")\r\n\r\n        # Two prompts ago - darker gray\r\n        if len(prompts) > 2:\r\n            older_prompt = prompts[-3]\r\n            truncated = truncate_prompt(older_prompt, 40)\r\n            parts.append(f\"\\033[90m{truncated}\\033[0m\")\r\n    else:\r\n        parts.append(\"\\033[90m💭 No prompts yet\\033[0m\")\r\n\r\n    # Join with separator\r\n    status_line = \" | \".join(parts)\r\n\r\n    return status_line\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Read JSON input from stdin\r\n        input_data = json.loads(sys.stdin.read())\r\n\r\n        # Generate status line\r\n        status_line = generate_status_line(input_data)\r\n\r\n        # Log the status line event (without error since it's successful)\r\n        log_status_line(input_data, status_line)\r\n\r\n        # Output the status line (first line of stdout becomes the status line)\r\n        print(status_line)\r\n\r\n        # Success\r\n        sys.exit(0)\r\n\r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully - output basic status\r\n        print(\"\\033[31m[Agent] [Claude] 💭 JSON Error\\033[0m\")\r\n        sys.exit(0)\r\n    except Exception as e:\r\n        # Handle any other errors gracefully - output basic status\r\n        print(f\"\\033[31m[Agent] [Claude] 💭 Error: {str(e)}\\033[0m\")\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    main()\r\n",
        "size": 5670
      },
      {
        "name": "status_line_v4.py",
        "path": "status_lines/status_line_v4.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\nimport json\r\nimport os\r\nimport sys\r\nfrom pathlib import Path\r\nfrom datetime import datetime\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\ndef log_status_line(input_data, status_line_output, error_message=None):\r\n    \"\"\"Log status line event to logs directory.\"\"\"\r\n    # Ensure logs directory exists\r\n    log_dir = Path(\"logs\")\r\n    log_dir.mkdir(parents=True, exist_ok=True)\r\n    log_file = log_dir / \"status_line.json\"\r\n\r\n    # Read existing log data or initialize empty list\r\n    if log_file.exists():\r\n        with open(log_file, \"r\") as f:\r\n            try:\r\n                log_data = json.load(f)\r\n            except (json.JSONDecodeError, ValueError):\r\n                log_data = []\r\n    else:\r\n        log_data = []\r\n\r\n    # Create log entry with input data and generated output\r\n    log_entry = {\r\n        \"timestamp\": datetime.now().isoformat(),\r\n        \"version\": \"v4\",\r\n        \"input_data\": input_data,\r\n        \"status_line_output\": status_line_output,\r\n    }\r\n\r\n    if error_message:\r\n        log_entry[\"error\"] = error_message\r\n\r\n    # Append the log entry\r\n    log_data.append(log_entry)\r\n\r\n    # Write back to file with formatting\r\n    with open(log_file, \"w\") as f:\r\n        json.dump(log_data, f, indent=2)\r\n\r\n\r\ndef get_session_data(session_id):\r\n    \"\"\"Get session data including agent name, prompts, and extras.\"\"\"\r\n    session_file = Path(f\".claude/data/sessions/{session_id}.json\")\r\n\r\n    if not session_file.exists():\r\n        return None, f\"Session file {session_file} does not exist\"\r\n\r\n    try:\r\n        with open(session_file, \"r\") as f:\r\n            session_data = json.load(f)\r\n            return session_data, None\r\n    except Exception as e:\r\n        return None, f\"Error reading session file: {str(e)}\"\r\n\r\n\r\ndef truncate_prompt(prompt, max_length=75):\r\n    \"\"\"Truncate prompt to specified length.\"\"\"\r\n    # Remove newlines and excessive whitespace\r\n    prompt = \" \".join(prompt.split())\r\n\r\n    if len(prompt) > max_length:\r\n        return prompt[: max_length - 3] + \"...\"\r\n    return prompt\r\n\r\n\r\ndef get_prompt_icon(prompt):\r\n    \"\"\"Get icon based on prompt type.\"\"\"\r\n    if prompt.startswith(\"/\"):\r\n        return \"⚡\"\r\n    elif \"?\" in prompt:\r\n        return \"❓\"\r\n    elif any(\r\n        word in prompt.lower()\r\n        for word in [\"create\", \"write\", \"add\", \"implement\", \"build\"]\r\n    ):\r\n        return \"💡\"\r\n    elif any(word in prompt.lower() for word in [\"fix\", \"debug\", \"error\", \"issue\"]):\r\n        return \"🐛\"\r\n    elif any(word in prompt.lower() for word in [\"refactor\", \"improve\", \"optimize\"]):\r\n        return \"♻️\"\r\n    else:\r\n        return \"💬\"\r\n\r\n\r\ndef format_extras(extras):\r\n    \"\"\"Format extras dictionary into a compact string.\"\"\"\r\n    if not extras:\r\n        return None\r\n    \r\n    # Format each key-value pair\r\n    pairs = []\r\n    for key, value in extras.items():\r\n        # Truncate value if too long\r\n        str_value = str(value)\r\n        if len(str_value) > 20:\r\n            str_value = str_value[:17] + \"...\"\r\n        pairs.append(f\"{key}:{str_value}\")\r\n    \r\n    return \" \".join(pairs)\r\n\r\n\r\ndef generate_status_line(input_data):\r\n    \"\"\"Generate the status line with agent name, most recent prompt, and extras.\"\"\"\r\n    # Extract session ID from input data\r\n    session_id = input_data.get(\"session_id\", \"unknown\")\r\n\r\n    # Get model name\r\n    model_info = input_data.get(\"model\", {})\r\n    model_name = model_info.get(\"display_name\", \"Claude\")\r\n\r\n    # Get session data\r\n    session_data, error = get_session_data(session_id)\r\n\r\n    if error:\r\n        # Log the error but show a default message\r\n        log_status_line(input_data, f\"[{model_name}] 💭 No session data\", error)\r\n        return f\"\\033[36m[{model_name}]\\033[0m \\033[90m💭 No session data\\033[0m\"\r\n\r\n    # Extract agent name, prompts, and extras\r\n    agent_name = session_data.get(\"agent_name\", \"Agent\")\r\n    prompts = session_data.get(\"prompts\", [])\r\n    extras = session_data.get(\"extras\", {})\r\n\r\n    # Build status line components\r\n    parts = []\r\n\r\n    # Agent name - Bright Red\r\n    parts.append(f\"\\033[91m[{agent_name}]\\033[0m\")\r\n\r\n    # Model name - Blue\r\n    parts.append(f\"\\033[34m[{model_name}]\\033[0m\")\r\n\r\n    # Most recent prompt\r\n    if prompts:\r\n        current_prompt = prompts[-1]\r\n        icon = get_prompt_icon(current_prompt)\r\n        truncated = truncate_prompt(current_prompt, 100)\r\n        parts.append(f\"{icon} \\033[97m{truncated}\\033[0m\")\r\n    else:\r\n        parts.append(\"\\033[90m💭 No prompts yet\\033[0m\")\r\n\r\n    # Add extras if they exist\r\n    if extras:\r\n        extras_str = format_extras(extras)\r\n        if extras_str:\r\n            # Display extras in cyan with brackets\r\n            parts.append(f\"\\033[36m[{extras_str}]\\033[0m\")\r\n\r\n    # Join with separator\r\n    status_line = \" | \".join(parts)\r\n\r\n    return status_line\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Read JSON input from stdin\r\n        input_data = json.loads(sys.stdin.read())\r\n\r\n        # Generate status line\r\n        status_line = generate_status_line(input_data)\r\n\r\n        # Log the status line event (without error since it's successful)\r\n        log_status_line(input_data, status_line)\r\n\r\n        # Output the status line (first line of stdout becomes the status line)\r\n        print(status_line)\r\n\r\n        # Success\r\n        sys.exit(0)\r\n\r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully - output basic status\r\n        print(\"\\033[31m[Agent] [Claude] 💭 JSON Error\\033[0m\")\r\n        sys.exit(0)\r\n    except Exception as e:\r\n        # Handle any other errors gracefully - output basic status\r\n        print(f\"\\033[31m[Agent] [Claude] 💭 Error: {str(e)}\\033[0m\")\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    main()",
        "size": 5923
      },
      {
        "name": "status_line_v5.py",
        "path": "status_lines/status_line_v5.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\n\"\"\"\r\nStatus Line v5 - Cost Tracking\r\nDisplay: [Model] $ $0.0123 | +156/-23 lines | 45s\r\nShows total_cost_usd, lines added/removed, duration\r\n\"\"\"\r\n\r\nimport json\r\nimport sys\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\n# ANSI color codes\r\nCYAN = \"\\033[36m\"\r\nGREEN = \"\\033[32m\"\r\nRED = \"\\033[31m\"\r\nYELLOW = \"\\033[33m\"\r\nBRIGHT_WHITE = \"\\033[97m\"\r\nDIM = \"\\033[90m\"\r\nRESET = \"\\033[0m\"\r\n\r\n\r\ndef format_cost(cost_usd):\r\n    \"\"\"Format cost with appropriate precision.\"\"\"\r\n    if cost_usd is None or cost_usd == 0:\r\n        return \"$0.00\"\r\n    elif cost_usd < 0.01:\r\n        return f\"${cost_usd:.4f}\"\r\n    elif cost_usd < 1.00:\r\n        return f\"${cost_usd:.3f}\"\r\n    else:\r\n        return f\"${cost_usd:.2f}\"\r\n\r\n\r\ndef format_duration(duration_ms):\r\n    \"\"\"Format duration in human-readable format.\"\"\"\r\n    if duration_ms is None or duration_ms == 0:\r\n        return \"0s\"\r\n\r\n    total_seconds = duration_ms / 1000\r\n\r\n    if total_seconds < 60:\r\n        return f\"{int(total_seconds)}s\"\r\n    elif total_seconds < 3600:\r\n        minutes = int(total_seconds // 60)\r\n        seconds = int(total_seconds % 60)\r\n        return f\"{minutes}m {seconds}s\"\r\n    else:\r\n        hours = int(total_seconds // 3600)\r\n        minutes = int((total_seconds % 3600) // 60)\r\n        return f\"{hours}h {minutes}m\"\r\n\r\n\r\ndef generate_status_line(input_data):\r\n    \"\"\"Generate the cost tracking status line.\"\"\"\r\n    # Get model name\r\n    model_info = input_data.get(\"model\", {})\r\n    model_name = model_info.get(\"display_name\", \"Claude\")\r\n\r\n    # Get cost data\r\n    cost_data = input_data.get(\"cost\", {})\r\n    total_cost = cost_data.get(\"total_cost_usd\", 0) or 0\r\n    total_duration = cost_data.get(\"total_duration_ms\", 0) or 0\r\n    lines_added = cost_data.get(\"total_lines_added\", 0) or 0\r\n    lines_removed = cost_data.get(\"total_lines_removed\", 0) or 0\r\n\r\n    # Format values\r\n    cost_str = format_cost(total_cost)\r\n    duration_str = format_duration(total_duration)\r\n\r\n    # Build status line\r\n    parts = []\r\n\r\n    # Model name in cyan\r\n    parts.append(f\"{CYAN}[{model_name}]{RESET}\")\r\n\r\n    # Cost with dollar sign emoji\r\n    parts.append(f\"{YELLOW}${RESET} {BRIGHT_WHITE}{cost_str}{RESET}\")\r\n\r\n    # Lines changed (green for added, red for removed)\r\n    lines_str = f\"{GREEN}+{lines_added}{RESET}/{RED}-{lines_removed}{RESET} lines\"\r\n    parts.append(lines_str)\r\n\r\n    # Duration with clock emoji\r\n    parts.append(f\"{DIM}@{RESET} {BRIGHT_WHITE}{duration_str}{RESET}\")\r\n\r\n    return \" | \".join(parts)\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Read JSON input from stdin\r\n        input_data = json.loads(sys.stdin.read())\r\n\r\n        # Generate status line\r\n        status_line = generate_status_line(input_data)\r\n\r\n        # Output the status line\r\n        print(status_line)\r\n\r\n        # Success\r\n        sys.exit(0)\r\n\r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully\r\n        print(f\"{RED}[Claude] $ Error: Invalid JSON{RESET}\")\r\n        sys.exit(0)\r\n    except Exception as e:\r\n        # Handle any other errors gracefully\r\n        print(f\"{RED}[Claude] $ Error: {str(e)}{RESET}\")\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    main()\r\n",
        "size": 3365
      },
      {
        "name": "status_line_v6.py",
        "path": "status_lines/status_line_v6.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\n\"\"\"\r\nStatus Line v6 - Context Window Usage\r\nDisplay: [Model] # [###---] | 42.5% used | ~115k left | session_id\r\nVisual progress indicator with percentage and session ID\r\n\"\"\"\r\n\r\nimport json\r\nimport sys\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\n# ANSI color codes\r\nCYAN = \"\\033[36m\"\r\nGREEN = \"\\033[32m\"\r\nYELLOW = \"\\033[33m\"\r\nRED = \"\\033[31m\"\r\nBRIGHT_WHITE = \"\\033[97m\"\r\nDIM = \"\\033[90m\"\r\nBLUE = \"\\033[34m\"\r\nMAGENTA = \"\\033[35m\"\r\nRESET = \"\\033[0m\"\r\n\r\n\r\ndef get_usage_color(percentage):\r\n    \"\"\"Get color based on usage percentage.\"\"\"\r\n    if percentage < 50:\r\n        return GREEN\r\n    elif percentage < 75:\r\n        return YELLOW\r\n    elif percentage < 90:\r\n        return RED\r\n    else:\r\n        return \"\\033[91m\"  # Bright red for critical\r\n\r\n\r\ndef create_progress_bar(percentage, width=15):\r\n    \"\"\"Create a visual progress bar.\"\"\"\r\n    filled = int((percentage / 100) * width)\r\n    empty = width - filled\r\n\r\n    color = get_usage_color(percentage)\r\n\r\n    # Use block characters for the bar\r\n    bar = f\"{color}{'#' * filled}{DIM}{'-' * empty}{RESET}\"\r\n    return f\"[{bar}]\"\r\n\r\n\r\ndef format_tokens(tokens):\r\n    \"\"\"Format token count in human-readable format.\"\"\"\r\n    if tokens is None:\r\n        return \"0\"\r\n    if tokens < 1000:\r\n        return str(int(tokens))\r\n    elif tokens < 1000000:\r\n        return f\"{tokens / 1000:.1f}k\"\r\n    else:\r\n        return f\"{tokens / 1000000:.2f}M\"\r\n\r\n\r\ndef generate_status_line(input_data):\r\n    \"\"\"Generate the context window usage status line.\"\"\"\r\n    # Get model name\r\n    model_info = input_data.get(\"model\", {})\r\n    model_name = model_info.get(\"display_name\", \"Claude\")\r\n\r\n    # Get session ID\r\n    session_id = input_data.get(\"session_id\", \"\") or \"--------\"\r\n\r\n    # Get context window data\r\n    context_data = input_data.get(\"context_window\", {})\r\n    used_percentage = context_data.get(\"used_percentage\", 0) or 0\r\n    context_window_size = context_data.get(\"context_window_size\", 200000) or 200000\r\n\r\n    # Calculate remaining tokens from used percentage\r\n    remaining_tokens = int(context_window_size * ((100 - used_percentage) / 100))\r\n\r\n    # Get color for percentage display\r\n    usage_color = get_usage_color(used_percentage)\r\n\r\n    # Build status line\r\n    parts = []\r\n\r\n    # Model name in cyan\r\n    parts.append(f\"{CYAN}[{model_name}]{RESET}\")\r\n\r\n    # Progress bar with hash indicator\r\n    progress_bar = create_progress_bar(used_percentage)\r\n    parts.append(f\"{MAGENTA}#{RESET} {progress_bar}\")\r\n\r\n    # Used percentage\r\n    parts.append(f\"{usage_color}{used_percentage:.1f}%{RESET} used\")\r\n\r\n    # Tokens left\r\n    tokens_left_str = format_tokens(remaining_tokens)\r\n    parts.append(f\"{BLUE}~{tokens_left_str} left{RESET}\")\r\n\r\n    # Session ID (rightmost)\r\n    parts.append(f\"{DIM}{session_id}{RESET}\")\r\n\r\n    return \" | \".join(parts)\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Read JSON input from stdin\r\n        input_data = json.loads(sys.stdin.read())\r\n\r\n        # Generate status line\r\n        status_line = generate_status_line(input_data)\r\n\r\n        # Output the status line\r\n        print(status_line)\r\n\r\n        # Success\r\n        sys.exit(0)\r\n\r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully\r\n        print(f\"{RED}[Claude] # Error: Invalid JSON{RESET}\")\r\n        sys.exit(0)\r\n    except Exception as e:\r\n        # Handle any other errors gracefully\r\n        print(f\"{RED}[Claude] # Error: {str(e)}{RESET}\")\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    main()\r\n",
        "size": 3690
      },
      {
        "name": "status_line_v7.py",
        "path": "status_lines/status_line_v7.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\n\"\"\"\r\nStatus Line v7 - Session Duration Timer\r\nDisplay: [Model] @ Session: 12m 34s | Started: 2:30 PM\r\nTrack session start time, show elapsed duration\r\n\"\"\"\r\n\r\nimport json\r\nimport sys\r\nfrom pathlib import Path\r\nfrom datetime import datetime\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\n# ANSI color codes\r\nCYAN = \"\\033[36m\"\r\nGREEN = \"\\033[32m\"\r\nYELLOW = \"\\033[33m\"\r\nRED = \"\\033[31m\"\r\nBRIGHT_WHITE = \"\\033[97m\"\r\nDIM = \"\\033[90m\"\r\nBLUE = \"\\033[34m\"\r\nMAGENTA = \"\\033[35m\"\r\nRESET = \"\\033[0m\"\r\n\r\n\r\n# Session tracking file\r\nSESSION_TIMES_FILE = Path.home() / \".claude\" / \"session_times.json\"\r\n\r\n\r\ndef get_session_start_time(session_id):\r\n    \"\"\"Get or create the session start time.\"\"\"\r\n    # Ensure directory exists\r\n    SESSION_TIMES_FILE.parent.mkdir(parents=True, exist_ok=True)\r\n\r\n    # Load existing session times\r\n    session_times = {}\r\n    if SESSION_TIMES_FILE.exists():\r\n        try:\r\n            with open(SESSION_TIMES_FILE, \"r\") as f:\r\n                session_times = json.load(f)\r\n        except (json.JSONDecodeError, ValueError):\r\n            session_times = {}\r\n\r\n    # Check if session already has a start time\r\n    if session_id in session_times:\r\n        return datetime.fromisoformat(session_times[session_id])\r\n\r\n    # Create new start time for this session\r\n    start_time = datetime.now()\r\n    session_times[session_id] = start_time.isoformat()\r\n\r\n    # Clean up old sessions (keep only last 50)\r\n    if len(session_times) > 50:\r\n        # Sort by time and keep most recent 50\r\n        sorted_sessions = sorted(session_times.items(), key=lambda x: x[1], reverse=True)\r\n        session_times = dict(sorted_sessions[:50])\r\n\r\n    # Save back to file\r\n    try:\r\n        with open(SESSION_TIMES_FILE, \"w\") as f:\r\n            json.dump(session_times, f, indent=2)\r\n    except Exception:\r\n        pass  # Ignore write errors\r\n\r\n    return start_time\r\n\r\n\r\ndef format_elapsed_time(start_time):\r\n    \"\"\"Format elapsed time since start.\"\"\"\r\n    elapsed = datetime.now() - start_time\r\n    total_seconds = int(elapsed.total_seconds())\r\n\r\n    if total_seconds < 60:\r\n        return f\"{total_seconds}s\"\r\n    elif total_seconds < 3600:\r\n        minutes = total_seconds // 60\r\n        seconds = total_seconds % 60\r\n        return f\"{minutes}m {seconds}s\"\r\n    else:\r\n        hours = total_seconds // 3600\r\n        minutes = (total_seconds % 3600) // 60\r\n        return f\"{hours}h {minutes}m\"\r\n\r\n\r\ndef format_start_time(start_time):\r\n    \"\"\"Format the start time in 12-hour format.\"\"\"\r\n    return start_time.strftime(\"%I:%M %p\").lstrip(\"0\")\r\n\r\n\r\ndef get_duration_color(start_time):\r\n    \"\"\"Get color based on session duration.\"\"\"\r\n    elapsed = datetime.now() - start_time\r\n    total_minutes = elapsed.total_seconds() / 60\r\n\r\n    if total_minutes < 30:\r\n        return GREEN\r\n    elif total_minutes < 60:\r\n        return YELLOW\r\n    elif total_minutes < 120:\r\n        return MAGENTA\r\n    else:\r\n        return RED\r\n\r\n\r\ndef generate_status_line(input_data):\r\n    \"\"\"Generate the session duration status line.\"\"\"\r\n    # Get model name\r\n    model_info = input_data.get(\"model\", {})\r\n    model_name = model_info.get(\"display_name\", \"Claude\")\r\n\r\n    # Get session ID\r\n    session_id = input_data.get(\"session_id\", \"default\")\r\n\r\n    # Get session start time\r\n    start_time = get_session_start_time(session_id)\r\n\r\n    # Format times\r\n    elapsed_str = format_elapsed_time(start_time)\r\n    started_str = format_start_time(start_time)\r\n    duration_color = get_duration_color(start_time)\r\n\r\n    # Build status line\r\n    parts = []\r\n\r\n    # Model name in cyan\r\n    parts.append(f\"{CYAN}[{model_name}]{RESET}\")\r\n\r\n    # Clock emoji and session duration\r\n    parts.append(f\"{BLUE}@{RESET} Session: {duration_color}{elapsed_str}{RESET}\")\r\n\r\n    # Start time\r\n    parts.append(f\"Started: {DIM}{started_str}{RESET}\")\r\n\r\n    return \" | \".join(parts)\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Read JSON input from stdin\r\n        input_data = json.loads(sys.stdin.read())\r\n\r\n        # Generate status line\r\n        status_line = generate_status_line(input_data)\r\n\r\n        # Output the status line\r\n        print(status_line)\r\n\r\n        # Success\r\n        sys.exit(0)\r\n\r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully\r\n        print(f\"{RED}[Claude] @ Error: Invalid JSON{RESET}\")\r\n        sys.exit(0)\r\n    except Exception as e:\r\n        # Handle any other errors gracefully\r\n        print(f\"{RED}[Claude] @ Error: {str(e)}{RESET}\")\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    main()\r\n",
        "size": 4726
      },
      {
        "name": "status_line_v8.py",
        "path": "status_lines/status_line_v8.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\n\"\"\"\r\nStatus Line v8 - Token Usage with Cache Stats\r\nDisplay: [Model] In: 15.2k | Out: 4.5k | Cache: ^5k v2k\r\nShow input/output tokens and cache creation/read stats\r\n\"\"\"\r\n\r\nimport json\r\nimport sys\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\n# ANSI color codes\r\nCYAN = \"\\033[36m\"\r\nGREEN = \"\\033[32m\"\r\nYELLOW = \"\\033[33m\"\r\nRED = \"\\033[31m\"\r\nBRIGHT_WHITE = \"\\033[97m\"\r\nDIM = \"\\033[90m\"\r\nBLUE = \"\\033[34m\"\r\nMAGENTA = \"\\033[35m\"\r\nRESET = \"\\033[0m\"\r\n\r\n\r\ndef format_tokens(tokens):\r\n    \"\"\"Format token count in human-readable format.\"\"\"\r\n    if tokens is None or tokens == 0:\r\n        return \"0\"\r\n    if tokens < 1000:\r\n        return str(int(tokens))\r\n    elif tokens < 10000:\r\n        return f\"{tokens / 1000:.1f}k\"\r\n    elif tokens < 1000000:\r\n        return f\"{tokens / 1000:.0f}k\"\r\n    else:\r\n        return f\"{tokens / 1000000:.1f}M\"\r\n\r\n\r\ndef generate_status_line(input_data):\r\n    \"\"\"Generate the token usage status line.\"\"\"\r\n    # Get model name\r\n    model_info = input_data.get(\"model\", {})\r\n    model_name = model_info.get(\"display_name\", \"Claude\")\r\n\r\n    # Get context window data\r\n    context_data = input_data.get(\"context_window\", {})\r\n    total_input = context_data.get(\"total_input_tokens\", 0) or 0\r\n    total_output = context_data.get(\"total_output_tokens\", 0) or 0\r\n\r\n    # Get current usage for cache stats\r\n    current_usage = context_data.get(\"current_usage\", {}) or {}\r\n    cache_creation = current_usage.get(\"cache_creation_input_tokens\", 0) or 0\r\n    cache_read = current_usage.get(\"cache_read_input_tokens\", 0) or 0\r\n\r\n    # Format token counts\r\n    input_str = format_tokens(total_input)\r\n    output_str = format_tokens(total_output)\r\n    cache_create_str = format_tokens(cache_creation)\r\n    cache_read_str = format_tokens(cache_read)\r\n\r\n    # Build status line\r\n    parts = []\r\n\r\n    # Model name in cyan\r\n    parts.append(f\"{CYAN}[{model_name}]{RESET}\")\r\n\r\n    # Chart emoji\r\n    parts.append(f\"{MAGENTA}#{RESET}\")\r\n\r\n    # Input tokens (incoming arrow)\r\n    parts.append(f\"In: {GREEN}{input_str}{RESET}\")\r\n\r\n    # Output tokens (outgoing)\r\n    parts.append(f\"Out: {YELLOW}{output_str}{RESET}\")\r\n\r\n    # Cache stats with up/down arrows\r\n    cache_str = f\"Cache: {BLUE}^{cache_create_str}{RESET} {DIM}v{cache_read_str}{RESET}\"\r\n    parts.append(cache_str)\r\n\r\n    return \" | \".join(parts)\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Read JSON input from stdin\r\n        input_data = json.loads(sys.stdin.read())\r\n\r\n        # Generate status line\r\n        status_line = generate_status_line(input_data)\r\n\r\n        # Output the status line\r\n        print(status_line)\r\n\r\n        # Success\r\n        sys.exit(0)\r\n\r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully\r\n        print(f\"{RED}[Claude] # Error: Invalid JSON{RESET}\")\r\n        sys.exit(0)\r\n    except Exception as e:\r\n        # Handle any other errors gracefully\r\n        print(f\"{RED}[Claude] # Error: {str(e)}{RESET}\")\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    main()\r\n",
        "size": 3202
      },
      {
        "name": "status_line_v9.py",
        "path": "status_lines/status_line_v9.py",
        "content": "#!/usr/bin/env -S uv run --script\r\n# /// script\r\n# requires-python = \">=3.11\"\r\n# dependencies = [\r\n#     \"python-dotenv\",\r\n# ]\r\n# ///\r\n\r\n\"\"\"\r\nStatus Line v9 - Minimal Powerline Style\r\nDisplay:  Opus  main  ~/project  42%\r\nUse Unicode powerline separators for minimal, clean, professional look\r\n\"\"\"\r\n\r\nimport json\r\nimport sys\r\nimport os\r\nimport subprocess\r\nfrom pathlib import Path\r\n\r\ntry:\r\n    from dotenv import load_dotenv\r\n\r\n    load_dotenv()\r\nexcept ImportError:\r\n    pass  # dotenv is optional\r\n\r\n\r\n# Powerline symbols\r\nPL_RIGHT = \"\\ue0b0\"  #\r\nPL_RIGHT_THIN = \"\\ue0b1\"  #\r\nPL_LEFT = \"\\ue0b2\"  #\r\nPL_LEFT_THIN = \"\\ue0b3\"  #\r\n\r\n# ANSI color codes - Background colors\r\nBG_BLUE = \"\\033[44m\"\r\nBG_GREEN = \"\\033[42m\"\r\nBG_YELLOW = \"\\033[43m\"\r\nBG_MAGENTA = \"\\033[45m\"\r\nBG_CYAN = \"\\033[46m\"\r\nBG_BLACK = \"\\033[40m\"\r\nBG_DEFAULT = \"\\033[49m\"\r\n\r\n# ANSI color codes - Foreground colors\r\nFG_BLUE = \"\\033[34m\"\r\nFG_GREEN = \"\\033[32m\"\r\nFG_YELLOW = \"\\033[33m\"\r\nFG_MAGENTA = \"\\033[35m\"\r\nFG_CYAN = \"\\033[36m\"\r\nFG_BLACK = \"\\033[30m\"\r\nFG_WHITE = \"\\033[97m\"\r\nFG_DEFAULT = \"\\033[39m\"\r\n\r\nBOLD = \"\\033[1m\"\r\nRESET = \"\\033[0m\"\r\n\r\n\r\ndef get_git_branch():\r\n    \"\"\"Get the current git branch name.\"\"\"\r\n    try:\r\n        result = subprocess.run(\r\n            [\"git\", \"branch\", \"--show-current\"],\r\n            capture_output=True,\r\n            text=True,\r\n            timeout=1\r\n        )\r\n        if result.returncode == 0:\r\n            branch = result.stdout.strip()\r\n            if branch:\r\n                return branch\r\n    except Exception:\r\n        pass\r\n\r\n    # Fallback: try reading .git/HEAD\r\n    try:\r\n        git_head = Path(\".git/HEAD\")\r\n        if git_head.exists():\r\n            content = git_head.read_text().strip()\r\n            if content.startswith(\"ref: refs/heads/\"):\r\n                return content.replace(\"ref: refs/heads/\", \"\")\r\n    except Exception:\r\n        pass\r\n\r\n    return None\r\n\r\n\r\ndef shorten_path(path, max_length=20):\r\n    \"\"\"Shorten a path for display.\"\"\"\r\n    if not path:\r\n        return \"~\"\r\n\r\n    # Replace home directory with ~\r\n    home = str(Path.home())\r\n    if path.startswith(home):\r\n        path = \"~\" + path[len(home):]\r\n\r\n    # If still too long, show only last parts\r\n    if len(path) > max_length:\r\n        parts = path.split(os.sep)\r\n        if len(parts) > 2:\r\n            # Show first and last parts\r\n            return f\"{parts[0]}/.../{parts[-1]}\"\r\n\r\n    return path\r\n\r\n\r\ndef get_usage_indicator(percentage):\r\n    \"\"\"Get a simple indicator based on context usage.\"\"\"\r\n    if percentage < 25:\r\n        return f\"{FG_GREEN}*{RESET}\"\r\n    elif percentage < 50:\r\n        return f\"{FG_GREEN}**{RESET}\"\r\n    elif percentage < 75:\r\n        return f\"{FG_YELLOW}***{RESET}\"\r\n    elif percentage < 90:\r\n        return f\"{FG_MAGENTA}****{RESET}\"\r\n    else:\r\n        return f\"{FG_MAGENTA}!!!!{RESET}\"\r\n\r\n\r\ndef generate_status_line(input_data):\r\n    \"\"\"Generate the minimal powerline status line.\"\"\"\r\n    # Get model name\r\n    model_info = input_data.get(\"model\", {})\r\n    model_name = model_info.get(\"display_name\", \"Claude\")\r\n\r\n    # Get workspace info\r\n    workspace = input_data.get(\"workspace\", {})\r\n    current_dir = workspace.get(\"current_dir\", os.getcwd())\r\n    short_path = shorten_path(current_dir)\r\n\r\n    # Get context usage\r\n    context_data = input_data.get(\"context_window\", {})\r\n    used_percentage = context_data.get(\"used_percentage\", 0) or 0\r\n\r\n    # Get git branch\r\n    git_branch = get_git_branch()\r\n\r\n    # Build powerline segments\r\n    segments = []\r\n\r\n    # Segment 1: Model name (blue background)\r\n    segments.append(f\"{BG_BLUE}{FG_BLACK}{BOLD} {model_name} {RESET}\")\r\n    segments.append(f\"{FG_BLUE}{BG_GREEN}{PL_RIGHT}{RESET}\")\r\n\r\n    # Segment 2: Git branch (green background) - only if in git repo\r\n    if git_branch:\r\n        segments.append(f\"{BG_GREEN}{FG_BLACK} {git_branch} {RESET}\")\r\n        segments.append(f\"{FG_GREEN}{BG_MAGENTA}{PL_RIGHT}{RESET}\")\r\n    else:\r\n        segments.append(f\"{FG_GREEN}{BG_MAGENTA}{PL_RIGHT}{RESET}\")\r\n\r\n    # Segment 3: Path (magenta background)\r\n    segments.append(f\"{BG_MAGENTA}{FG_BLACK} {short_path} {RESET}\")\r\n    segments.append(f\"{FG_MAGENTA}{BG_CYAN}{PL_RIGHT}{RESET}\")\r\n\r\n    # Segment 4: Context usage (cyan background)\r\n    segments.append(f\"{BG_CYAN}{FG_BLACK} {used_percentage:.0f}% {RESET}\")\r\n    segments.append(f\"{FG_CYAN}{PL_RIGHT}{RESET}\")\r\n\r\n    return \"\".join(segments)\r\n\r\n\r\ndef main():\r\n    try:\r\n        # Read JSON input from stdin\r\n        input_data = json.loads(sys.stdin.read())\r\n\r\n        # Generate status line\r\n        status_line = generate_status_line(input_data)\r\n\r\n        # Output the status line\r\n        print(status_line)\r\n\r\n        # Success\r\n        sys.exit(0)\r\n\r\n    except json.JSONDecodeError:\r\n        # Handle JSON decode errors gracefully\r\n        print(f\"{BG_MAGENTA}{FG_BLACK} Error {RESET}{FG_MAGENTA}{PL_RIGHT}{RESET}\")\r\n        sys.exit(0)\r\n    except Exception:\r\n        # Handle any other errors gracefully\r\n        print(f\"{BG_MAGENTA}{FG_BLACK} Error {RESET}{FG_MAGENTA}{PL_RIGHT}{RESET}\")\r\n        sys.exit(0)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    main()\r\n",
        "size": 5092
      }
    ]
  },
  "shell_analysis": {
    "findings": [
      {
        "pattern_id": "destructive_command",
        "severity": "high",
        "description": "Recursive or forced file deletion",
        "command": "    Matches various forms of rm -rf and similar destructive patterns.\r"
      },
      {
        "pattern_id": "destructive_command",
        "severity": "high",
        "description": "Recursive or forced file deletion",
        "command": "        r'\\brm\\s+.*-[a-z]*r[a-z]*f',  # rm -rf, rm -fr, rm -Rf, etc.\r"
      },
      {
        "pattern_id": "destructive_command",
        "severity": "high",
        "description": "Recursive or forced file deletion",
        "command": "        r'\\brm\\s+--recursive\\s+--force',  # rm --recursive --force\r"
      },
      {
        "pattern_id": "destructive_command",
        "severity": "high",
        "description": "Recursive or forced file deletion",
        "command": "        r'\\brm\\s+--force\\s+--recursive',  # rm --force --recursive\r"
      },
      {
        "pattern_id": "destructive_command",
        "severity": "high",
        "description": "Recursive or forced file deletion",
        "command": "        r'\\*',          # Wildcards in general rm -rf context\r"
      },
      {
        "pattern_id": "credential_access",
        "severity": "high",
        "description": "Accesses credential files or sensitive configuration",
        "command": "    Check if any tool is trying to access .env files containing sensitive data.\r"
      },
      {
        "pattern_id": "credential_access",
        "severity": "high",
        "description": "Accesses credential files or sensitive configuration",
        "command": "            if '.env' in file_path and not file_path.endswith('.env.sample'):\r"
      },
      {
        "pattern_id": "credential_access",
        "severity": "high",
        "description": "Accesses credential files or sensitive configuration",
        "command": "                r'\\b\\.env\\b(?!\\.sample)',  # .env but not .env.sample\r"
      },
      {
        "pattern_id": "credential_access",
        "severity": "high",
        "description": "Accesses credential files or sensitive configuration",
        "command": "                r'cat\\s+.*\\.env\\b(?!\\.sample)',  # cat .env\r"
      },
      {
        "pattern_id": "credential_access",
        "severity": "high",
        "description": "Accesses credential files or sensitive configuration",
        "command": "                r'echo\\s+.*>\\s*\\.env\\b(?!\\.sample)',  # echo > .env\r"
      },
      {
        "pattern_id": "credential_access",
        "severity": "high",
        "description": "Accesses credential files or sensitive configuration",
        "command": "                r'touch\\s+.*\\.env\\b(?!\\.sample)',  # touch .env\r"
      },
      {
        "pattern_id": "credential_access",
        "severity": "high",
        "description": "Accesses credential files or sensitive configuration",
        "command": "                r'cp\\s+.*\\.env\\b(?!\\.sample)',  # cp .env\r"
      },
      {
        "pattern_id": "credential_access",
        "severity": "high",
        "description": "Accesses credential files or sensitive configuration",
        "command": "                r'mv\\s+.*\\.env\\b(?!\\.sample)',  # mv .env\r"
      },
      {
        "pattern_id": "credential_access",
        "severity": "high",
        "description": "Accesses credential files or sensitive configuration",
        "command": "            print(\"BLOCKED: Access to .env files containing sensitive data is prohibited\", file=sys.stderr)\r"
      },
      {
        "pattern_id": "credential_access",
        "severity": "high",
        "description": "Accesses credential files or sensitive configuration",
        "command": "            print(\"Use .env.sample for template files instead\", file=sys.stderr)\r"
      }
    ]
  },
  "instruction_file": {
    "relative_path": "README.md",
    "absolute_path": "C:\\Users\\scott\\Desktop\\Projects\\contract-pipeline\\pipeline\\data\\repos\\disler\\claude-code-hooks-mastery\\README.md",
    "hash": "sha256:015ecce2df32992d031179dd8a5deff2b8c32147370e80d1658b62fc9d89b149",
    "exists": true
  },
  "gitSha": "052ad1cbd5aeb1ec4a1def22012d1293c6225625"
}