# Hook patterns Hooks are compiled from spore decisions where the policy is fully deterministic. They fire via the cchooks runtime before the scion reasons, at zero token cost. Install cchooks: `pip install cchooks` or `uv add cchooks` --- ## When to generate a hook vs. keep as spore-only Generate a hook when: - The trigger condition is pattern-matchable without contextual judgment - The outcome is always the same given the trigger (no "it depends") - The decision can be encoded in ~20 lines of Python Keep as spore-only when: - The decision requires the scion to interpret context before acting - The outcome varies based on factors not visible in the tool call metadata - The policy involves weighing tradeoffs A spore entry should always exist for every hook (provenance). Not every spore entry needs a hook (judgment). --- ## File write guard The most common hook type for annotator and transformer task classes. Blocks writes to sensitive paths before the scion acts. ```python #!/usr/bin/env python3 # hooks/env-guard.py # Spore: env-file-write-guard # Blocks writes to credential and sensitive config files. from cchooks import create_context, PreToolUseContext SENSITIVE_PATTERNS = {".env", "secrets.json", "id_rsa", ".pem", ".key"} c = create_context() assert isinstance(c, PreToolUseContext) if c.tool_name == "Write": file_path = c.tool_input.get("file_path", "") if any(pattern in file_path for pattern in SENSITIVE_PATTERNS): c.output.deny( reason=f"Credential file protected: {file_path}", system_message="Sensitive file write blocked by spore policy. Escalate to parent if this write is intentional." ) else: c.output.allow() else: c.output.allow() ``` --- ## Bash command guard Blocks destructive or elevated-privilege bash patterns. Appropriate for any task class that does not require shell execution. ```python #!/usr/bin/env python3 # hooks/bash-guard.py # Spore: destructive-bash-guard # Blocks bash commands matching destructive or privilege-escalation patterns. from cchooks import create_context, PreToolUseContext BLOCKED_PATTERNS = ["rm -rf", "sudo", "fdisk", "format", "dd if=", "mkfs"] c = create_context() assert isinstance(c, PreToolUseContext) if c.tool_name == "Bash": command = c.tool_input.get("command", "") for pattern in BLOCKED_PATTERNS: if pattern in command: c.output.deny( reason=f"Blocked pattern in bash command: {pattern}", system_message="Destructive command blocked by spore policy. Escalate to parent if this command is required." ) break else: c.output.allow() else: c.output.allow() ``` --- ## Output format enforcer Fires after a write and validates that output conforms to the required format. Appropriate for annotator task classes with strict output contracts. ```python #!/usr/bin/env python3 # hooks/output-format-validator.py # Spore: annotation-output-format # Validates that annotation output files are valid JSONL. import json from cchooks import create_context, PostToolUseContext c = create_context() assert isinstance(c, PostToolUseContext) if c.tool_name == "Write" and c.tool_input.get("file_path", "").endswith(".annotation"): content = c.tool_input.get("content", "") lines = [l.strip() for l in content.strip().splitlines() if l.strip()] invalid_lines = [] for i, line in enumerate(lines): try: json.loads(line) except json.JSONDecodeError: invalid_lines.append(i + 1) if invalid_lines: c.output.challenge( reason=f"Output format violation: lines {invalid_lines} are not valid JSON", system_message="Annotation output must be JSONL (one JSON object per line). Correct and retry." ) else: c.output.accept() else: c.output.accept() ``` --- ## Session start context loader Fires when the scion session starts and loads the spore file path into context. Use this to ensure the scion always reads its spore file on startup. ```python #!/usr/bin/env python3 # hooks/session-start.py # Loads spore file contents into session context on startup. import os from cchooks import create_context, SessionStartContext SPORE_FILE = os.environ.get("SCION_SPORE_FILE", "") c = create_context() assert isinstance(c, SessionStartContext) if c.source == "startup" and SPORE_FILE and os.path.exists(SPORE_FILE): with open(SPORE_FILE) as f: content = f.read() print(f"Spore policy loaded from {SPORE_FILE}:\n\n{content}") c.output.exit_success() ``` Set `SCION_SPORE_FILE` in the scion's environment at spawn time. The commissioning skill is responsible for setting this variable to the correct spore file path. --- ## Hook registration in claude settings Register hooks in `.claude/settings.json` at the project or user level: ```json { "hooks": { "PreToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "python hooks/env-guard.py" } ] }, { "matcher": "Bash", "hooks": [ { "type": "command", "command": "python hooks/bash-guard.py" } ] } ], "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "python hooks/output-format-validator.py" } ] } ], "SessionStart": [ { "hooks": [ { "type": "command", "command": "python hooks/session-start.py" } ] } ] } } ``` --- ## Validation before registration Every hook must be validated before the commissioning skill registers it. Minimal validation: run the hook with a synthetic input that should trigger the deny/challenge condition and confirm it fires correctly. ```bash # Test env-guard with a synthetic Write event targeting .env echo '{"tool_name": "Write", "tool_input": {"file_path": ".env", "content": "SECRET=x"}}' \ | python hooks/env-guard.py # Expected: exit code 2 (deny) with reason message # Test env-guard with a safe path — should allow echo '{"tool_name": "Write", "tool_input": {"file_path": "output.txt", "content": "hello"}}' \ | python hooks/env-guard.py # Expected: exit code 0 (allow) ``` If a hook does not pass both cases, do not register it and do not reference it in the spore entry. Fix or remove before depositing.