Posture, Receipts, and Hooks¶
The Phase 2 and Phase 3 components in outline. Implementations land at v0.2.0a0 and v0.3.0a0 respectively; what's described here is the contract those phases fill in.
Manifest (Phase 2)¶
A manifest is the policy document for a tool. It declares:
- The tool's name and signature.
- The set of effects each invocation can produce.
- Posture constraints — under which postures the tool may be invoked, and under which it must be refused.
Manifests are validated as Pydantic v2 models and round-trip authored fixtures byte-for-byte after JSON normalisation.
Classifier (Phase 2)¶
def classify(tool_call: ToolCall, manifest: Manifest) -> Decision: ...
Pure function. Given a tool call and a manifest, returns a Decision carrying:
effects: frozenset[Effect]— every effect the call can produce.most_restrictive: Effect— the dominant class.rationale: str— human-readable explanation.posture_transition: PostureTransition | None— what the posture machine should do next.
No I/O. No clocks. Same input → same output, every time.
Posture state machine¶
The closed Posture enum lands in Phase 2 (manifest validation depends on it). Transition functions land in Phase 3.
Posture is the current operational mode. Transitions (Phase 3) are pure value-in-value-out functions; no hidden state.
| Posture | Value | Meaning |
|---|---|---|
INTERACTIVE |
"interactive" |
Operator is at the keyboard; ambiguous calls escalate to a prompt. |
AUTONOMOUS |
"autonomous" |
No operator in the loop; ambiguous calls fail closed. |
DRY_RUN |
"dry_run" |
Classification only; no WRITE/NETWORK/EXECUTE/SPAWN/DESTRUCTIVE effects fire. |
LOCKED |
"locked" |
Refuse everything except explicitly allow-listed read-only calls. |
Transitions are total — every (posture, decision) pair has a defined next posture or a PostureError. There are no implicit transitions.
Receipt (Phase 3)¶
A structured record of a single decision. Field ordering is deterministic; serialised receipts hash to identical bytes given identical inputs.
A receipt carries:
- The input tool call (as the manifest validated it).
- The classified effects and dominant class.
- The posture before and after.
- The decision (
allow,deny,escalate). - A content hash that uniquely identifies the receipt.
No timestamps. The hook layer attaches wall-clock time as out-of-band metadata; the receipt itself stays content-addressable.
Hook (Phase 3)¶
A thin I/O wrapper. Reads a hook payload from stdin, runs the pipeline, writes a decision to stdout, exits 0 (allow) or non-zero (deny).
The hook is the only place in the package that touches sys.stdin or sys.stdout. Everything inside is pure.
# Phase 3 — wired into Claude Code as a PreToolUse hook
spine-lite hook < payload.json
echo $? # 0 = allow, non-zero = deny
CLI (Phase 3)¶
| Subcommand | Status | Purpose |
|---|---|---|
version |
Phase 1 — shipped | Print the installed version. |
validate-manifest |
Phase 2 | Validate a manifest file against the schema. |
classify |
Phase 2 | One-shot classification of a tool call. |
hook |
Phase 3 | stdin/stdout PreToolUse adapter for Claude Code. |
See also¶
- Concepts / Overview — pipeline shape.
- How-To / Wire into Claude Code — operator runbook.
- Explanation / Porting Notes — design history and the relationship to the sibling project.