Initial commit — Singular Particular Space v1

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>
This commit is contained in:
2026-03-27 12:09:22 +02:00
commit 5422131782
359 changed files with 117437 additions and 0 deletions

View File

@@ -0,0 +1,366 @@
# Hook protocol
Claude Code hooks are executables that intercept agent lifecycle events. They
require no SDK or external library — the protocol is just stdin JSON in,
exit code out. This file contains everything needed to generate correct hooks
in Python (stdlib only) or JavaScript (Node.js, no packages).
---
## The protocol
Claude Code spawns the hook executable, writes a JSON event to its stdin, and
reads the exit code and stdout when it exits.
**Exit codes:**
- `0` — allow / success. Claude Code continues. Any stdout is added to context.
- `1` — non-blocking error. Claude Code sees the message and continues anyway.
- `2` — blocking deny. Claude Code stops the action and shows the reason.
**JSON control output** (optional, write to stdout as valid JSON):
For `PreToolUse` hooks, you can output a JSON object instead of relying solely
on the exit code. This gives you a reason string that Claude Code surfaces to
the agent:
```json
{"decision": "block", "reason": "Credential file write blocked by spore policy"}
```
```json
{"decision": "approve"}
```
For `PostToolUse` hooks, stdout text (not JSON) is injected into the agent's
context as tool result feedback.
**Input event shape** (arrives on stdin as a single JSON line):
```json
{
"tool_name": "Write",
"tool_input": {
"file_path": "path/to/file",
"content": "file content"
},
"tool_response": "...",
"session_id": "abc123"
}
```
`tool_response` is only present on `PostToolUse` events.
`tool_input` shape varies by tool — see field names below.
**Common tool_input fields by tool:**
- `Write` / `Edit` / `MultiEdit`: `file_path`, `content`
- `Bash`: `command`
- `Read`: `file_path`
- `WebFetch`: `url`
---
## Python template (stdlib only)
Use this for projects that have Python available. Zero dependencies — no pip
install, no virtualenv. Copy, rename, fill in the guard logic.
```python
#!/usr/bin/env python3
# hooks/{hook-name}.py
# Spore: {spore-identifier}
# {one-line description of what this hook guards}
#
# Protocol: reads JSON event from stdin, exits 0 (allow) or 2 (deny).
# No external dependencies — stdlib only.
import json
import sys
def main() -> None:
# Read the full event from stdin
try:
event = json.loads(sys.stdin.read())
except (json.JSONDecodeError, OSError):
# If we can't read the event, allow and let the agent handle it
sys.exit(0)
tool_name = event.get("tool_name", "")
tool_input = event.get("tool_input", {})
# --- Guard logic starts here ---
# Replace this block with the compiled spore decision.
# Example: block writes to credential files.
if tool_name == "Write":
file_path = tool_input.get("file_path", "")
BLOCKED = {".env", "secrets.json", "id_rsa", ".pem", ".key"}
if any(pattern in file_path for pattern in BLOCKED):
# Write structured denial to stdout for Claude Code to surface
print(json.dumps({
"decision": "block",
"reason": f"Credential file protected by spore policy: {file_path}"
}))
sys.exit(2)
# --- Guard logic ends here ---
# Default: allow
sys.exit(0)
if __name__ == "__main__":
main()
```
---
## JavaScript template (Node.js, no packages)
Use this for TypeScript/JavaScript projects, or any project where Node.js is
available (Claude Code itself runs on Node so it is always present). Zero
dependencies — pure Node.js stdlib.
```javascript
#!/usr/bin/env node
// hooks/{hook-name}.js
// Spore: {spore-identifier}
// {one-line description of what this hook guards}
//
// Protocol: reads JSON event from stdin, exits 0 (allow) or 2 (deny).
// No external dependencies — Node.js stdlib only.
async function main() {
// Read the full event from stdin
let raw = "";
for await (const chunk of process.stdin) {
raw += chunk;
}
let event;
try {
event = JSON.parse(raw);
} catch {
// If we can't parse the event, allow and let the agent handle it
process.exit(0);
}
const toolName = event.tool_name ?? "";
const toolInput = event.tool_input ?? {};
// --- Guard logic starts here ---
// Replace this block with the compiled spore decision.
// Example: block writes to credential files.
if (toolName === "Write") {
const filePath = toolInput.file_path ?? "";
const blocked = [".env", "secrets.json", "id_rsa", ".pem", ".key"];
if (blocked.some((pattern) => filePath.includes(pattern))) {
// Write structured denial to stdout for Claude Code to surface
process.stdout.write(JSON.stringify({
decision: "block",
reason: `Credential file protected by spore policy: ${filePath}`,
}));
process.exit(2);
}
}
// --- Guard logic ends here ---
// Default: allow
process.exit(0);
}
main().catch(() => process.exit(0)); // On unexpected error, fail open
```
---
## PostToolUse validator template (Python)
PostToolUse hooks receive the completed tool result and can inject feedback
into the agent's context. Use for output format validation, quality checks,
or logging.
```python
#!/usr/bin/env python3
# hooks/{hook-name}-post.py
# Spore: {spore-identifier}
# Validates output after a tool completes. Injects feedback into agent context.
import json
import sys
def main() -> None:
try:
event = json.loads(sys.stdin.read())
except (json.JSONDecodeError, OSError):
sys.exit(0)
tool_name = event.get("tool_name", "")
tool_input = event.get("tool_input", {})
# tool_response contains what the tool returned
tool_response = event.get("tool_response", "")
# --- Validation logic starts here ---
# Example: verify annotation output is valid JSONL
if tool_name == "Write":
file_path = tool_input.get("file_path", "")
if file_path.endswith(".annotation"):
content = tool_input.get("content", "")
invalid = []
for i, line in enumerate(content.strip().splitlines(), 1):
if line.strip():
try:
json.loads(line)
except json.JSONDecodeError:
invalid.append(i)
if invalid:
# Exit 2 to block; message shown to agent
print(json.dumps({
"decision": "block",
"reason": f"Output format violation: lines {invalid} are not valid JSON. "
f"Annotation output must be JSONL (one JSON object per line)."
}))
sys.exit(2)
# --- Validation logic ends here ---
# Exit 0; any stdout text gets injected into agent context as feedback
sys.exit(0)
if __name__ == "__main__":
main()
```
---
## SessionStart context loader template (Python)
Loads the spore file into session context on startup so the scion has its
policy available from the first turn.
```python
#!/usr/bin/env python3
# hooks/session-start.py
# Loads SCION_SPORE_FILE contents into session context on startup.
# The commissioning skill sets SCION_SPORE_FILE at spawn time.
import json
import os
import sys
def main() -> None:
try:
event = json.loads(sys.stdin.read())
except (json.JSONDecodeError, OSError):
sys.exit(0)
# Only act on startup, not resume or clear
if event.get("source") != "startup":
sys.exit(0)
spore_file = os.environ.get("SCION_SPORE_FILE", "")
if not spore_file or not os.path.exists(spore_file):
# No spore file configured — allow and continue
sys.exit(0)
try:
with open(spore_file) as f:
content = f.read()
# Print to stdout — SessionStart hook output is added to session context
print(f"Spore policy active ({spore_file}):\n\n{content}")
except OSError:
pass # Fail open — don't block startup if spore file unreadable
sys.exit(0)
if __name__ == "__main__":
main()
```
---
## Registration in .claude/settings.json
Register hooks at the module level (`.claude/settings.json` in the module
folder) rather than globally. This keeps hook scope aligned with spore scope.
```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-validator-post.py" }
]
}
],
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "python hooks/session-start.py" }
]
}
]
}
}
```
For Node.js hooks, replace `python hooks/name.py` with `node hooks/name.js`.
For executable hooks (chmod +x), the command can be the file path directly.
---
## Choosing Python vs JavaScript
Use Python when the project uses Python or when no preference is stated —
Python stdlib is universally available and the syntax reads clearly to most
agents. Use JavaScript (Node.js) when the project is a TypeScript/JavaScript
codebase, or when the hook needs to share logic with existing project tooling.
The bartolli pattern (`.claude/hooks/{project-type}/quality-check.js`) is
worth adopting when multiple hook types share configuration — see that repo
for a mature example of project-type-aware Node.js hooks with SHA256 config
caching for performance.
Either language produces identical protocol behavior. The hook protocol does
not care what generates the exit code.
---
## Validation before registration
Every generated hook must pass two test cases before the commissioning skill
registers it: one that should trigger the deny condition, and one that should
allow cleanly. Run manually:
```bash
# Should exit 2 (deny) — triggers the guard
echo '{"tool_name":"Write","tool_input":{"file_path":".env","content":"SECRET=x"}}' \
| python hooks/env-guard.py
echo "Exit code: $?"
# Should exit 0 (allow) — safe path
echo '{"tool_name":"Write","tool_input":{"file_path":"output.txt","content":"hello"}}' \
| python hooks/env-guard.py
echo "Exit code: $?"
```
If either test fails, do not register the hook or reference it in a spore entry.
Fix first, validate again, then deposit.