#!/usr/bin/env python3 """Compile hooks.template.json + prompt .md files into hooks.json. Usage: python3 hooks/scripts/compile-hooks.py # Generate hooks.json python3 hooks/scripts/compile-hooks.py --check # Verify hooks.json is up to date """ import json import sys from pathlib import Path HOOKS_DIR = Path(__file__).resolve().parent.parent TEMPLATE = HOOKS_DIR / "hooks.template.json" OUTPUT = HOOKS_DIR / "hooks.json" PROMPTS_DIR = HOOKS_DIR / "prompts" def load_prompt(filename: str) -> str: """Load a prompt .md file and return its content as a string.""" path = PROMPTS_DIR / filename if not path.exists(): print(f"ERROR: Prompt file not found: {path}", file=sys.stderr) sys.exit(1) content = path.read_text(encoding="utf-8") if not content.strip(): print(f"ERROR: Prompt file is empty: {path}", file=sys.stderr) sys.exit(1) return content.rstrip("\n") def resolve_prompts(obj): """Recursively walk JSON and replace prompt_file with inline prompt.""" if isinstance(obj, dict): if "prompt_file" in obj: if obj.get("type") != "prompt": print( f"ERROR: prompt_file used on non-prompt hook type: {obj.get('type')}", file=sys.stderr, ) sys.exit(1) filename = obj.pop("prompt_file") obj["prompt"] = load_prompt(filename) return {k: resolve_prompts(v) for k, v in obj.items()} if isinstance(obj, list): return [resolve_prompts(item) for item in obj] return obj def compile_hooks() -> str: """Read template, resolve prompts, return JSON string.""" if not TEMPLATE.exists(): print(f"ERROR: Template not found: {TEMPLATE}", file=sys.stderr) sys.exit(1) template = json.loads(TEMPLATE.read_text(encoding="utf-8")) resolved = resolve_prompts(template) # Strip any top-level keys except "hooks" — Claude Code requires only "hooks" invalid_keys = [k for k in resolved if k != "hooks"] for k in invalid_keys: print(f"WARNING: Stripping invalid top-level key '{k}' from output", file=sys.stderr) del resolved[k] return json.dumps(resolved, indent=2, ensure_ascii=False) + "\n" def main(): check_mode = "--check" in sys.argv compiled = compile_hooks() if check_mode: if not OUTPUT.exists(): print(f"ERROR: {OUTPUT} does not exist", file=sys.stderr) sys.exit(1) current = OUTPUT.read_text(encoding="utf-8") if current == compiled: print("OK: hooks.json is up to date") sys.exit(0) else: print( "DRIFT DETECTED: hooks.json does not match compiled output.\n" "Run: python3 hooks/scripts/compile-hooks.py", file=sys.stderr, ) sys.exit(1) OUTPUT.write_text(compiled, encoding="utf-8") print(f"Compiled {OUTPUT.relative_to(HOOKS_DIR.parent)}") if __name__ == "__main__": main()