Skip to main content

Hook-file existence guard (settings.json ↔ disk integrity) — MC #103640

Hook-file existence guard (settings.json ↔ disk integrity)

Book: System Architecture Status: Implemented + self-verified — MC #103640 (2026-06-15) Commits: 7408f0170 (restore 22 hooks, ~/.claude) · 8f7b8e602 (existence guard, ~/system)


Incident that motivated this

On 2026-06-15 the CEO flagged that "someone did stupid things with skills/hooks." Tool-forensics found ~/.claude/settings.json registered 76 hook entries while 22 of the referenced gate FILES did not exist on disk (absent from ~/.claude, ~/system, and ~/backups). Every tool call was invoking non-existent gates → silently dead enforcement.

Root cause (per the CEO's own commit 568e9cee0 / MC #103627): a "previous session had left a no-op stub" — a prior session stubbed/deleted registered hooks. The files were never removed by a tracked commit (git log --diff-filter=D empty on the HEAD line); they lived only as working-tree files synced from [BACKUP] commits and vanished from disk.

Missing gates included critical security/claim enforcers: secret-scanner, git-author-guard, alai-claim-gate, evidence-contract-validator, pre-publish-claims-gate, john-determinism-gate, claim-auto-probe-gate, +15.

Why it went undetected

lint-hooks.sh verified that REQUIRED hooks were registered in settings.json (correct event / matcher / ordering, via substring match) — but it never checked that each registered hook's script file actually exists on disk. The daily com.john.hook-drift-detector-v2 runs lint-hooks.sh, so the same blind spot meant the daily drift detector also missed it.

The fix

  1. Restore — all 22 missing gate hooks restored from canonical git history (5f7dc6ad5 MC#99730, 79f92e3f9 MC#99197, dated auto-backups) → commit 7408f0170. Audit went 22 → 0 missing.
  2. Guard (lint-hooks.sh) — new EXISTENCE pass extracts every hook command's script path (/Users/* and ~/* .sh/.py/.js) and verifies os.path.exists. Missing → FAIL, counted into the summary and exit 2. Because the daily drift detector already runs lint-hooks.sh, this is enforced daily with no new schedule.
  3. Boot surface (boot.sh) — SessionStart "Hook integrity" check prints EXISTENCE N present / N referenced and lists any MISSING-on-disk files via ok()/fail(), so the CEO sees it at every boot.

Verification

  • bash -n lint-hooks.sh / bash -n boot.sh → PASS.
  • Clean state: EXISTENCE 46 hook file(s) present / 46 referenced, 0 missing.
  • Regression: renamed secret-scanner.sh away → FAIL [file-exists:secret-scanner.sh] MISSING ON DISK + exit 2; file restored after test.
  • Closure passed restored gates live: mc-ready-gate.sh (evidence-json) → evidence-contract-validator.sh CONFIRMED → ZAKON #30 direct-probe gate.

Known separate drift (out of scope, logged)

userprompt-cost-guard.sh is not registered in UserPromptSubmit (a registration-drift, the inverse problem — file may exist but isn't wired). Surfaced by lint-hooks.sh as a pre-existing FAIL; tracked for follow-up.

Re-verification — 2026-07-28 (MC #101472)

Sibling MC #101482 (same T-A-08 genesis) was left BLOCKED on 2026-07-02 by review over two bugs: (1) hook-drift-detector.sh captured $(cmd) || true before reading $?, masking the real linter exit code; (2) evidence claimed live_run_exit=0 while 4 .bak cruft files were still present.

Live re-check this session found both already fixed on disk (landed with the existence-guard commit 07df8d121a above):

  • bash lint-hooks.sh against live settings.json: 68 PASS / 0 WARN / 0 FAIL, EXISTENCE 51/51 hook files present, exit 0.
  • No || true between the Phase 1/2 command substitution and its $? read in hook-drift-detector.sh — masking bug gone.
  • find ~/.claude/hooks -maxdepth 1 -name "*.bak" → 0 files.
  • bash hook-drift-detector.sh --dry-run against live settings → RESULT: CLEAN, exit 0.
  • Synthetic-drift test (the exact follow-up the #101482 review asked for): ran a throwaway copy of the wrapper pointed at a stub linter that emits a FAIL and exit 2 — wrapper correctly reported RESULT: DRIFT DETECTED and returned exit 2. Confirms the wrapper no longer swallows real drift.
  • LaunchAgent com.john.hook-drift-detector-v2 confirmed registered, state=active, daily 07:00, last exit code=0.

Evidence: /tmp/evidence-101472/verification.json, /tmp/evidence-101472/verification.md.