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