Homepage (site/index.html): integration-v14 promoted, Writings section integrated with 33 pieces clustered by type (stories/essays/miscellany), Writings welcome lightbox, content frame at 98% opacity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
227 lines
6.3 KiB
Markdown
227 lines
6.3 KiB
Markdown
# 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.
|