Architecture¶
Mission¶
Classify and decide on every LLM tool call before it runs. Pure, deterministic, byte-for-byte reproducible. Wire into Claude Code as a PreToolUse hook; carry the same semantics anywhere else a hook contract exists.
The runtime in one diagram¶
┌──────────────────────────────────────────────┐
│ pure pipeline │
│ │
stdin payload ──► │ manifest ──► classifier ──► posture machine │ ──► stdout decision
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ManifestError Classification- Posture- │
│ Error Error │
│ │
│ receipt (deterministic bytes) │
└──────────────────────────────────────────────┘
exit 0 = allow │
exit n = deny ▼
receipt directory (optional)
The hook is a thin I/O wrapper. Everything inside is pure: no clocks, no randomness, no network, no filesystem. Same input → same output, every time.
Module map¶
| Module | Responsibility | Pure | Phase |
|---|---|---|---|
effects |
Closed six-class taxonomy + precedence + most_restrictive |
yes | 1 ✓ |
exceptions |
Hierarchy rooted at SpineLiteError |
yes | 1 ✓ |
manifest |
Tool-definition schema (Pydantic v2) | yes | 2 |
classifier |
Tool call → effect set → Decision |
yes | 2 |
posture |
Posture state machine | yes | 3 |
receipt |
Structured, deterministic decision records | yes | 3 |
hook |
Claude Code PreToolUse stdin/stdout adapter | I/O | 3 |
cli |
Operator interface (Typer) | I/O | 1 (version) → 3 (full) |
The five pure modules contain no clocks, no randomness, no network, no filesystem. Determinism is the contract.
Why the pure / I/O split¶
Determinism only holds if there's exactly one boundary where wall-clock entropy enters. Everything else has to be pure. The hook is that boundary; the CLI is that boundary; nothing else is.
This is what makes a receipt content-addressable. A receipt that hashed differently every time you ran it would be useless for replay; the only way to get byte-stability is to keep the hot path free of timestamps, UUIDs, and randomness. The cost is one extra layer of indirection (the hook); the benefit is replayability by SHA forever.
Why a closed taxonomy¶
A taxonomy that grows at runtime is a taxonomy that drifts. The six classes were chosen to draw the lines that matter for safety review:
READvs.WRITE: did anything change?WRITEvs.NETWORK: did anything cross a trust boundary?NETWORKvs.EXECUTE: did we hand control to another process?EXECUTEvs.SPAWN: did the child outlive the call?SPAWNvs.DESTRUCTIVE: can we undo it?
Anything finer is a manifest concern, not a taxonomy concern. If something feels like a seventh class, it's almost always an existing class with manifest-level qualifiers — which file the WRITE touched, which host the NETWORK call hit.
Adding a class is permitted but expensive. The process is in Effects Taxonomy / Why closed and triggers a HALT to the project lead.
Phase plan¶
| Phase | Modules | Version | What you can do |
|---|---|---|---|
| 1 (shipped) | effects, exceptions, cli (version) |
v0.1.0a0 |
Collapse effect sets, raise/catch typed errors, ship as a package. |
| 2 | + manifest, classifier |
v0.2.0a0 |
Classify a tool call against a manifest, get a Decision. |
| 3 | + posture, receipt, hook, cli (full) |
v0.3.0a0 |
End-to-end PreToolUse integration with Claude Code. |
The architecture stays the same across phases. Each phase fills in pure modules that adhere to the same purity contract.
Sibling project¶
MacFall7/M87-Spine-lite is a Python sibling project: a governance framework for Claude Code shell commands with a different six-class taxonomy drawn on shell-vs-file lines. It's informational and citable, not a parity target. See Porting Notes for the full relationship.
spine-lite-python's spec is canonical and lives in this repository: the architectural invariants in CLAUDE.md, the Invariants page, and the design rationale recorded as decisions are made.
See also¶
- Invariants — the rules nothing in this repo gets to break.
- Design Rationale — past decisions, with reasoning.
- Porting Notes — TypeScript-to-Python translation log.
- FAQ — common questions about the design.