TradingAgents/.claude/hooks/validate_settings_hooks.py

144 lines
4.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Validate Settings Template Hooks - Pre-commit Hook
Ensures hooks referenced in global_settings_template.json actually exist
in the hooks directory. Prevents "hook not found" errors after install.
Usage:
python3 validate_settings_hooks.py
Exit Codes:
0 - All referenced hooks exist
1 - Some hooks are missing
"""
import json
import re
import sys
from pathlib import Path
def get_project_root() -> Path:
"""Find project root by looking for .git directory."""
current = Path.cwd()
while current != current.parent:
if (current / ".git").exists():
return current
current = current.parent
return Path.cwd()
def extract_hook_files(settings: dict) -> list[str]:
"""Extract hook file names from settings template.
Returns:
List of hook filenames (e.g., ['pre_tool_use.py', 'auto_git_workflow.py'])
"""
hooks = []
hooks_config = settings.get("hooks", {})
for lifecycle, matchers in hooks_config.items():
if not isinstance(matchers, list):
continue
for matcher in matchers:
if not isinstance(matcher, dict):
continue
for hook in matcher.get("hooks", []):
if not isinstance(hook, dict):
continue
command = hook.get("command", "")
# Extract hook filename from command like:
# "python3 ~/.claude/hooks/pre_tool_use.py"
# "MCP_AUTO_APPROVE=true python3 ~/.claude/hooks/pre_tool_use.py"
match = re.search(r'hooks/([a-z_]+\.py)', command)
if match:
hooks.append(match.group(1))
return hooks
def validate_settings_hooks() -> tuple[bool, list[str]]:
"""Validate all hooks in settings template exist AND are in install manifest.
IMPORTANT: Hooks must be both:
1. Present in source (plugins/autonomous-dev/hooks/)
2. Listed in install_manifest.json (so they get installed to ~/.claude/hooks/)
Returns:
Tuple of (success, list of error messages)
"""
project_root = get_project_root()
plugin_dir = project_root / "plugins" / "autonomous-dev"
# Load settings template
template_path = plugin_dir / "config" / "global_settings_template.json"
if not template_path.exists():
return True, [] # No template, nothing to validate
try:
settings = json.loads(template_path.read_text())
except json.JSONDecodeError as e:
return False, [f"Invalid JSON in settings template: {e}"]
# Load install manifest
manifest_path = plugin_dir / "config" / "install_manifest.json"
manifest_hooks = set()
if manifest_path.exists():
try:
manifest = json.loads(manifest_path.read_text())
manifest_hooks = {
Path(p).name
for p in manifest.get("components", {}).get("hooks", {}).get("files", [])
}
except json.JSONDecodeError:
pass # Will be caught by other validation
# Extract referenced hooks
referenced_hooks = extract_hook_files(settings)
if not referenced_hooks:
return True, [] # No hooks referenced
# Check each hook exists in source AND manifest
hooks_dir = plugin_dir / "hooks"
errors = []
for hook_file in referenced_hooks:
hook_path = hooks_dir / hook_file
# Check 1: Exists in source
if not hook_path.exists():
errors.append(f"{hook_file}: Missing from source directory")
continue
# Check 2: Listed in manifest (so it gets installed)
if hook_file not in manifest_hooks:
errors.append(
f"{hook_file}: Exists in source but NOT in install_manifest.json! "
f"This hook won't be installed to ~/.claude/hooks/"
)
return len(errors) == 0, errors
def main() -> int:
"""Main entry point."""
success, missing = validate_settings_hooks()
if success:
print("✅ All settings template hooks exist")
return 0
else:
print("❌ Settings template references missing hooks!")
print("")
print("Missing hooks:")
for hook in sorted(missing):
print(f" - {hook}")
print("")
print("Fix: Either create the hook or update global_settings_template.json")
return 1
if __name__ == "__main__":
sys.exit(main())