Design — proofpunk v1.10.0: deterministic hook enforcement + /proofpunk:install
Status: implemented in this release. Sources cited per section.
0. Research basis (all external, verified 2026-08-13)
| Source | What it contributed |
|---|---|
| Claude Code Hooks reference (code.claude.com/docs/en/hooks) | 30 lifecycle events; decision-control matrix (decision:"block", hookSpecificOutput.permissionDecision, additionalContext); matcher grammar; ${CLAUDE_PLUGIN_ROOT}; prompt-hook response schema |
anthropics/claude-code plugin-dev/skills/hook-development |
Canonical Stop/SubagentStop completeness-check pattern (block feeds reason back to Claude to continue); PreToolUse JSON output shape |
| claude-mem hooks architecture doc | Performance discipline: hooks sub-100ms, always-exit-0 on Setup, version-marker pattern, suppressOutput where appropriate |
| CLAUDE.md current best practices (Anthropic memory docs + luongnv89/claude-howto + community) | ≤200 lines (adherence degrades past it); imperative voice; high-priority rules in first 40 lines; NO verification reminders (Opus-5-era over-verification); .claude/rules/*.md with paths: frontmatter for scoped loading; never clobber — marked-section merge |
Design decisions traceable to the table:
- Stop guard is a command hook, not a prompt hook. The official canonical example for Stop is prompt-based (Haiku judges completeness). We use a deterministic command hook instead: (a) it is locally testable end-to-end (a prompt hook's judgment can't be measured without an LLM in the loop — and this release must be measured); (b) zero latency/cost per stop; (c) the heuristic is narrow and documented, blocking only on a strong unproven-claim signal, defaulting to non-blocking additionalContext.
- PreToolUse guards are command hooks (fast, deterministic, JSON deny) — prompt hooks for path checks would be waste; the docs' own
.envexample is a command hook for exactly this class. - CLAUDE.md ≤ 200 lines is a hard acceptance criterion of
/proofpunk:install.
1. Hook set (hooks/hooks.json)
| Event | Matcher | Type | Script | Behavior |
|---|---|---|---|---|
| SessionStart | startup|resume|clear |
command | session-start.sh (existing, tightened) |
doctrine additionalContext; <20ms; exit 0 |
| Stop | (none) | command | stop-guard.sh |
transcript scan (see §2): strong unproven-claim → {"decision":"block","reason":…} (reason is fed back to Claude and the turn continues); otherwise silent (exit 0, no output) — corrected in v1.10.1 after a live multi-plugin session showed per-stop reminder noise; hook discipline: speak only when enforcing |
| SubagentStop | (none) | command | stop-guard.sh |
same check, subagent transcript |
| PreToolUse | Write|Edit |
command | evidence-guard.sh |
deny (exit 2 + stderr) when the write targets an evidence dir and payload matches secret patterns; else allow (exit 0) |
| InstructionsLoaded | (none) | command | instructions-loaded.sh |
append {ts, filePath, loadReason, cwd} to ~/.claude/proofpunk-loads.jsonl; always exit 0 (the measurement tap for /proofpunk:install) |
2. stop-guard.sh — the unproven-completion detector
Inputs (stdin JSON): session_id, transcript_path, cwd, hook_event_name.
Algorithm (deterministic, documented heuristic):
- Read the last 40 lines of
transcript_path(JSONL). No file → exit 0. - Claim signal: an assistant text line containing a completion marker (
\b(done|complete|completed|finished|shipped|all tests pass|works now)\bcase-insensitive). - Proof signal: any line in the same window referencing a proof artifact (
e2e-evidence/,evidence-inventory,step-\d+,screenshot,curl .*200,verdict). - Block iff claim-signal AND NOT proof-signal:
{"decision":"block","reason":"Proofpunk: completion was claimed without a cited end-user test artifact. Run the end-user test (drive the real system), capture run-scoped evidence, cite it by full path — or downgrade the claim to UNVERIFIED."} - Else: exit 0 with no output. (v1.10.0 shipped a soft
additionalContextreminder here; in real sessions with a dozen-plus plugins that is feedback spam on every stop. SessionStart owns doctrine context; Stop speaks only to block.)
False-positive posture: the block reason tells Claude exactly how to satisfy the guard (produce evidence or downgrade), so a mistaken block costs one turn of clarification, not a halt. This mirrors the official pattern where the reason is the next instruction.
3. evidence-guard.sh — secrets never enter evidence
Inputs: tool_name, tool_input.file_path (Write/Edit).
- If
file_pathmatches*(e2e-evidence|evidence)/*: scantool_input.contentfor secret patterns (sk-[A-Za-z0-9_-]{16,},ghp_[A-Za-z0-9]{20,},AKIA[0-9A-Z]{16},BEGIN (RSA|OPENSSH|EC|PGP) PRIVATE KEY,api[_-]?key). Match → exit 2, stderr: "Proofpunk: refusing to write probable secret material into an evidence directory (see evidence-contract: redact, never commit)." - Else exit 0.
4. /proofpunk:install — project memory installer (7th command)
Command doc: commands/install.md. The agent executes four steps with explicit verification after each:
- Detect — stack manifests (package.json/pyproject/go.mod/Cargo.toml), test/build commands, existing
CLAUDE.md, existing.claude/rules/, project name from the root directory. - CLAUDE.md — write or merge:
- Template (
assets/claude-md-template.md): ≤40 lines of proofpunk doctrine + detected commands. Total file MUST stay ≤200 lines. - Existing file → merge into
<!-- proofpunk:begin -->…<!-- proofpunk:end -->section (replace section if present, append if absent). Never edit outside the markers. Report the diff.
- Template (
- Scoped rules —
.claude/rules/:proof-obligations.md(nopaths:— global): task-level proof rule.evidence-contract.md(paths: ["e2e-evidence/**", "evidence/**"]): redact + freshness rules, loads only when evidence files are touched.tui-driving.md(paths: ["**/*.tsx", "**/ink*"], only when the project is a TUI): the tui-testing discipline pointer.
- Verify — read the files back (line counts), print the InstructionsLoaded log line proving the last load event (
~/.claude/proofpunk-loads.jsonltail), and state the acceptance criteria: CLAUDE.md ≤200 lines, markers present, rules parse.
opencode variant: opencode/commands/proofpunk-install.md (same playbook).
5. Measurement plan (before → after, all executed, none claimed)
| Metric | v1.9.0 | v1.10.0 | Instrument |
|---|---|---|---|
| Hook events covered | 1 | 5 | hooks.json diff |
| Deterministic doctrine enforcement | 0 blocking hooks | 3 decision points (Stop, SubagentStop, PreToolUse) | tools/test-hooks.sh run |
| Commands (claude + opencode) | 6+6 | 7+7 | file count |
| Project-memory scaffolding | none | CLAUDE.md + 3 scoped rules via one command | dry-run in /tmp/pp-init-fixture |
| Hook-script correctness | untested | bash -n + 8 JSON-stdin behavior cases green | test output committed |
| Orchestration graph | closed | closed (verifier exit 0) | verify-orchestration.py |
| Site | v1.9.0 pages | v1.10.0 + install command row + hooks section | regenerated |
Acceptance: every row measured by a script whose output is committed to evidence/hooks-release/.
source → plugins/proofpunk/docs/hooks-and-init-design.md · ← all docs