100%

Section 08 — Route-Access Enforcement

  • Cites §01 north star (invariants 1, 2).
  • Grounded in Pass 1D/N1 (block-spec-edits.sh pattern; Read not guarded today).
  • Operationalizes “LLM does zero routing reads; scripts are the sole accessor.”

Intelligence Reconnaissance

Run 2026-05-29:

  • scripts/intel-query.sh dag-ascii scripts-first-restructure — §08 linear-walk position §06 → §07 → §07A → §08; depends_on: ["07A"] (sole direct predecessor per INV-19 single-successor, re-pointed from §07 when §07A was inserted between §07 and §08; §07 complete + reviewed; §06/§07 reachable transitively via §07A).
  • scripts/intel-query.sh plan-status scripts-first-restructure — §06 complete; §07 complete; §07A in-review; §08 in-progress; §09-§12 not-started.
  • Remaining bullets are direct file citations (PreToolUse hooks + settings are not intel-graph-indexed; they anchor on the source file):
  • [ori:.claude/hooks/block-spec-edits.sh] — the deny-JSON precedent (hookSpecificOutput.permissionDecision: deny at :75-77); guards Edit/Write ONLY (the N1 gap §08.2 closes by adding the Read/Grep/Glob matcher).
  • [ori:.claude/settings.json] — already groups Read|Grep|Glob at a PreToolUse matcher slot (:66), so banning reads is wiring (not new matcher infrastructure); §08.2 registers block-route-edits.sh there + for Edit/Write/Bash.
  • [ori:plans/scripts-first-restructure/decisions/01-completion-integrity-invariants.md] §7 — INV-17 bypass-expiry; gates retirement of the persistent .claude/.plan-corpus-bootstrap-active marker on §08 close (the new scoped+expiring unlock REPLACES it).

Goal

See frontmatter. The hook makes the no-plan-reading invariant mechanical, not disciplinary.

  • Scope: plan.json (under plans/ + bug-tracker/plans/) is the §08 deliverable. Bug-tracker aggregate-index read-banning (open-bugs.json / closed-bugs.json) is a separate follow-up under §10 corpus migration, NOT absorbed here — keeps the §08 deliverable bounded to per-plan routing state.

08.1 — Author block-route-edits.sh

  • Author .claude/hooks/block-route-edits.sh modeled on block-spec-edits.sh: read stdin → extract the target path → match */plan.json under plans/ or bug-tracker/plans/ → emit the deny JSON (hookSpecificOutput.permissionDecision: deny) per block-spec-edits.sh:74-85.
  • Extract a UNION of tool_input path keys (not file_path alone): tool_input.file_path (Edit/Write), plus the Read/Grep/Glob path args (tool_input.path, tool_input.pattern-derived dir) — so the hook matches */plan.json across all tool schemas. The settings.json Read|Grep|Glob matcher grouping (wired in §08.2) feeds these tools to the hook.
  • Also match Bash reads of plan.json (cat/head/grep/jq/sed/tail/python -c against */plan.json) so the ban can’t be shell-bypassed.
  • Design note — Bash-read guard is BEST-EFFORT: Bash command parsing is fragile against quoting, globs, command substitution, and obfuscated readers. Accept-over-block tradeoff: prefer a false-positive denial (a borderline command blocked) over a silent bypass. The deterministic guarantee is the tool-schema path-key union above; the Bash matcher is a defense-in-depth layer, not the primary enforcement surface.
  • Allow content/**/*.md edits (markdown content stays LLM-writable).
  • Subsection close (08.1) — all [x]; status: complete.

08.2 — settings.json Read-tool wiring + user-typed unlock

  • Wire the hook for the Read tool (N1: block-spec-edits.sh only guards Edit/Write; banning READS requires the Read matcher in settings.json) AND Edit/Write/Bash.
  • User-typed unlock: a literal --explicit-user-confirm flag OR .claude/.route-unlock-active marker (Locked-Designs shape per CLAUDE.md §Locked Designs); Claude NEVER synthesizes it; one scoped access per unlock.
  • INV-17 bypass-expiry (per decisions/01-completion-integrity-invariants.md §3 Class-C + §7): the unlock marker MUST carry a scope (path/op) + an owning-reference or expiry; the hook honors it for exactly one scoped access then re-locks, and refuses an expired / archived-owner / stale marker (enforces normally). No env-var or marker grants a persistent corpus-wide allow.
  • Single-use mechanism — the hook stays a PURE GATE (READ-ONLY; never mutates filesystem state per INV-17, matching the block-spec-edits.sh precedent it is modeled on and the §07.2 pure-gate-hook invariant). The unlock marker .claude/.route-unlock-active carries {scope: path/op, owning_reference, expiry}; the hook READS it and grants iff scope matches AND not-expired AND owning-reference live, else DENIES (enforces normally). Single-use is owned by the marker’s LIFECYCLE owner OUTSIDE the hook — the user-typed --explicit-user-confirm unlock command writes the marker with a one-shot scope + near-term expiry; the route API / engine (or the natural expiry window) clears it after the scoped op. The expiry + owning-reference are what make a stale persistent allow unrepresentable (NOT hook mutation). The hook NEVER writes — a mutating PreToolUse hook is the INV-17 violation §07.2 already banned.
  • Retire the persistent .plan-corpus-bootstrap-active marker + its bypass logic from its REAL home scripts/plan_corpus/hook_dispatch.py (marker read at :54-56/:290/:294; ORI_HOOK_DISPATCH_BYPASS allow paths at :429-436) — NOT .claude/hooks/*.sh (the marker is absent there; a shell-only grep would no-op). Removal gated on §08 per decisions/01-completion-integrity-invariants.md §7. The new scoped+expiring unlock REPLACES it — the two never coexist. Strip the bootstrap-marker branch + the persistent-global-allow env path from hook_dispatch.py.
  • Subsection close (08.2) — all [x]; status: complete.

08.3 — Rule extension + tests

  • Extend plan-read-discipline.md §2 (WHO reads plan files) with the plan.json read-ban + the route-substrate access contract (scripts/route API are the sole accessor); add only a one-line state-location pointer to state-discipline.md §2 cross-referencing it. The read-ban is a who-reads rule (plan-read-discipline.md owns it), NOT a where-state-lives rule (state-discipline.md). Out-of-compiler-rigor rule edit.
  • Tests: hook denies Read/Edit/Write/Bash-read of a plan.json fixture; allows content/<id>--*.md edit; allows script-mediated access (no hook fire on the route API path); unlock marker permits exactly one scoped access then re-locks.
  • Negative pins (Bash-read deny matrix): cat/head/grep/jq/sed/tail/python -c each invoked against a protected */plan.json fixture is DENIED (one pin per reader command).
  • Negative pin (no persistent global-allow): after a single-use unlock grant + scoped access, assert NO persistent global-allow marker survives — .plan-corpus-bootstrap-active is absent from hook_dispatch.py’s read paths AND .claude/.route-unlock-active is past-expiry / cleared by its lifecycle owner (the hook itself never mutates it); a second access on an expired/stale marker is DENIED by the read-only scope+expiry check.
  • Subsection close (08.3) — all [x]; status: complete.

08.4 — Enforcement-completeness hardening (Grep/Glob directory-scope deny + double-fire test)

The §08 success criterion “denies tool calls touching <any>/plan.json” has a real hole: block-route-edits.sh extracts the path key and matches only when it ends in plan.json, so a Grep path=plans/<plan> (a DIRECTORY) or Glob pattern=plans/<plan>/** recursively surfaces plan.json CONTENT without the literal plan.json token in the path key — the deny is bypassed and scripts are NOT yet the sole accessor (the §08 + umbrella INV-1 invariant).

  • Deny Grep/Glob whose path key is a DIRECTORY under plans/ or bug-tracker/plans/ that recursively contains a plan.json (block-route-edits.sh:47-92 _literal_dir_prefix + _scope_surfaces_plan_json helpers + the step-2b Grep/Glob branch: search root lexically within a protected prefix AND a depth-bounded plan.json glob hit → deny; repo-root/above-plans scan stays best-effort residual).
  • Negative pins in scripts/plan_corpus/tests/test_block_route_edits.py: Grep path=plans/foo → deny (test_grep_plan_directory_denies); Grep path=plans/foo/content → allow (test_grep_content_subdir_without_plan_json_allows); Glob pattern=plans/foo/** → deny (test_glob_pattern_plan_directory_denies); plus plans-root, bug-tracker/plans-root, Glob path-base, and non-plan-dir allow pins (8 new; 29 pass total).
  • Defense-in-depth ordering pin (AR-1): test_both_hooks_deny_plan_json_edit_order_independent asserts a plan.json Edit is denied by block-route-edits.sh AND by hook_dispatch.decide() independently — either deny suffices regardless of settings.json hook order.
  • BS-2 best-effort note: extended the block-route-edits.sh:58-66 step-2 comment to name find/-exec + xargs reader-chains as the accepted-over-blocked residual (deterministic guarantee = tool-schema path-key union + the step-2b Grep/Glob directory-scope deny).
  • Round-2 blind-spots hardening landed (committed): block-route-edits.sh:_scope_surfaces_plan_json now realpath-normalizes the search root (closes the ..-traversal escape, agy-F1) and uses a recursive protected-subtree glob (closes deep bug-tracker/plans/completed/<bug>/plan.json, agy-F3); 3 new pins in scripts/plan_corpus/tests/test_block_route_edits.py (test_grep_bug_tracker_root_catches_completed_plan_json, test_grep_traversal_into_protected_denies, test_grep_traversal_out_of_protected_allows); 32 block-route tests pass.
  • Subsection close (08.4) — all [x]; status: complete.

08.R Third Party Review Findings

  • None.

08.N Completion Checklist

  • 08.1-08.4 [x] and status: complete.
  • Every frontmatter success_criteria item maps to a complete subsection deliverable (the criteria are plain YAML list items with no body checkbox to flip; this item tracks deliverable-coverage, not a nonexistent body [x]): block-route-edits.sh authored + path-key union + Bash-read guard + content/*.md allow → §08.1; Read-tool wiring + user-typed unlock + INV-17 scope/expiry + pure-gate + bootstrap-marker retirement → §08.2 (committed 0fb38c52); plan-read-discipline.md §2 rule extension + deny/allow/unlock tests + Bash-read deny matrix + no-persistent-global-allow negative pin → §08.3; Grep/Glob directory-scope deny + double-fire ordering pin → §08.4.
  • Hook self-test green (deny matrix + allow content + unlock).
  • python -m scripts.plan_corpus check plans/scripts-first-restructure/section-08-*.md exit 0. exit 0 (verified this session)
  • /tpr-review passed (final, full-section). /review-plan ran 2 /tpr-review rounds; exit_reason clean; SIGNIFICANT REWORK APPLIED; reviewed:true; third_party_review.status:clean

References

  • Pass 1D/N1: .claude/hooks/block-spec-edits.sh (deny pattern; Edit/Write-only today).
  • CLAUDE.md §Locked Designs (user-typed unlock shape); state-discipline.md §2.
  • §01 invariants 1, 2.

HISTORY

  • 2026-05-29 — Autopilot auto-cure: review_plan_redispatch_loop reset_lost_dispatch (autopilot_run_id=72ece7c1a87349c08ab6dcbd6367019d); chain log recorded a phantom /review-plan dispatch but section frontmatter showed zero review evidence; chain counter reset and /review-plan re-dispatched (per state-discipline.md §4 Hard-abort terminal-state semantics + skill-control-contract.md §Autopilot Mode unified hook-failure continuation clause).

  • 2026-05-30 — §08.1 complete; §08.2 marker-retirement design dependency surfaced (autopilot): §08.1 block-route-edits.sh authored + verified (Read/Edit/Write/Bash/Grep/Glob deny matrix; pattern-key added; content/*.md + non-plan allow; live .route-unlock-active scope+owner+expiry grant honored) — committed. §08.2 settings-wiring (Bash/Edit/Write/Read|Grep|Glob matchers), the --explicit-user-confirm unlock marker, INV-17 scope+expiry, and the pure-gate (read-only hook) are DONE. The §08.2 .plan-corpus-bootstrap-active marker-retirement is BLOCKED by a discovered design dependency: scripts/plan_corpus/hook_dispatch.py is a SECOND PreToolUse hook (settings.json :38/:58/:78) whose is_plan_path blocks Edit/Write/Bash on EVERY plan-path — plans/**/*.md (section files) + content/**/*.md + plan.json + bug-tracker/** — currently DISABLED by the bootstrap marker. Naively removing the marker re-enables that broad ban across all active sessions, blocking all plan-markdown editing and contradicting §08’s “markdown content stays LLM-editable; only plan.json banned” invariant. Correct safe resolution (records the path, NOT yet applied): narrow hook_dispatch.PLAN_PATH_GLOBS to JSON-routing-files only (plans/**/plan.json + bug-tracker JSON-indexes/plan.json + schemas/v5/**), EXCLUDING plan/content .md, so markdown stays editable; THEN strip the ORI_HOOK_DISPATCH_BYPASS env-bypass (no production consumer — only its own --self-test + test_pretool_hook.py) + the .plan-corpus-bootstrap-active marker check (hook_dispatch.py:290/:294) + remove the marker file + update docstring/self-test/test_pretool_hook.py + add the §08.3 negative pin (no persistent global-allow survives). Security-boundary change with cross-session blast radius (every parallel session’s plan-file Edit-tool enforcement flips immediately) — the same change the prior session paused for user discussion; left for a user touchpoint rather than unilaterally re-enabled under autopilot.

  • 2026-05-30 — §08.2 marker-retirement LANDED (supersedes prior BLOCKED entry): the immediately-preceding 2026-05-30 entry’s “BLOCKED / records the path, NOT yet applied / left for a user touchpoint” status is SUPERSEDED — the user authorized the unlock this session and the retirement is committed at 0fb38c52 (“scripts-first §08.2 — narrow route ban to plan.json + retire bootstrap-marker bypass”). Landed: hook_dispatch.PLAN_PATH_GLOBS narrowed to plans/**/plan.json + bug-tracker/plans/**/plan.json + schemas/v5/** (plan/content .md excluded — markdown stays LLM-editable); .plan-corpus-bootstrap-active marker check + ORI_HOOK_DISPATCH_BYPASS env-bypass removed; marker file deleted; §08.3 negative pin added at scripts/plan_corpus/tests/test_block_route_edits.py (test_no_unlock_marker_denies). §08.2 status: complete. The prior BLOCKED entry is retained per append-only HISTORY discipline; this entry is the current truth.

  • 2026-05-30 — Autopilot auto-cure: review_plan_redispatch_loop detected; record_review_abort restored status to ‘in-progress’; advancing autopilot (per state-discipline.md §4 Hard-abort terminal-state semantics + skill-control-contract.md §Autopilot Mode unified hook-failure continuation clause).