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— §06complete; §07complete; §07Ain-review; §08in-progress; §09-§12not-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: denyat:75-77); guards Edit/Write ONLY (the N1 gap §08.2 closes by adding the Read/Grep/Glob matcher).[ori:.claude/settings.json]— already groupsRead|Grep|Globat a PreToolUse matcher slot (:66), so banning reads is wiring (not new matcher infrastructure); §08.2 registersblock-route-edits.shthere + 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-activemarker 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(underplans/+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.shmodeled onblock-spec-edits.sh: read stdin → extract the target path → match*/plan.jsonunderplans/orbug-tracker/plans/→ emit the deny JSON (hookSpecificOutput.permissionDecision: deny) perblock-spec-edits.sh:74-85. - Extract a UNION of
tool_inputpath keys (notfile_pathalone):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.jsonacross all tool schemas. The settings.jsonRead|Grep|Globmatcher 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 -cagainst*/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/**/*.mdedits (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
Readtool (N1:block-spec-edits.shonly guards Edit/Write; banning READS requires the Read matcher in settings.json) AND Edit/Write/Bash. - User-typed unlock: a literal
--explicit-user-confirmflag OR.claude/.route-unlock-activemarker (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.shprecedent it is modeled on and the §07.2 pure-gate-hook invariant). The unlock marker.claude/.route-unlock-activecarries{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-confirmunlock 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-activemarker + its bypass logic from its REAL homescripts/plan_corpus/hook_dispatch.py(marker read at:54-56/:290/:294;ORI_HOOK_DISPATCH_BYPASSallow paths at:429-436) — NOT.claude/hooks/*.sh(the marker is absent there; a shell-only grep would no-op). Removal gated on §08 perdecisions/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 fromhook_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 tostate-discipline.md §2cross-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.jsonfixture; allowscontent/<id>--*.mdedit; 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 -ceach invoked against a protected*/plan.jsonfixture 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-activeis absent fromhook_dispatch.py’s read paths AND.claude/.route-unlock-activeis 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/orbug-tracker/plans/that recursively contains aplan.json(block-route-edits.sh:47-92_literal_dir_prefix+_scope_surfaces_plan_jsonhelpers + the step-2b Grep/Glob branch: search root lexically within a protected prefix AND a depth-boundedplan.jsonglob 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); plusplans-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_independentasserts aplan.jsonEdit is denied byblock-route-edits.shAND byhook_dispatch.decide()independently — either deny suffices regardless of settings.json hook order. - BS-2 best-effort note: extended the
block-route-edits.sh:58-66step-2 comment to namefind/-exec+xargsreader-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_jsonnow realpath-normalizes the search root (closes the..-traversal escape, agy-F1) and uses a recursive protected-subtree glob (closes deepbug-tracker/plans/completed/<bug>/plan.json, agy-F3); 3 new pins inscripts/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]andstatus: complete. - Every frontmatter
success_criteriaitem 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.shauthored + 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 (committed0fb38c52);plan-read-discipline.md §2rule 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-*.mdexit 0. exit 0 (verified this session) -
/tpr-reviewpassed (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.shauthored + verified (Read/Edit/Write/Bash/Grep/Glob deny matrix;pattern-key added; content/*.md + non-plan allow; live.route-unlock-activescope+owner+expiry grant honored) — committed. §08.2 settings-wiring (Bash/Edit/Write/Read|Grep|Glob matchers), the--explicit-user-confirmunlock marker, INV-17 scope+expiry, and the pure-gate (read-only hook) are DONE. The §08.2.plan-corpus-bootstrap-activemarker-retirement is BLOCKED by a discovered design dependency:scripts/plan_corpus/hook_dispatch.pyis a SECOND PreToolUse hook (settings.json :38/:58/:78) whoseis_plan_pathblocks 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): narrowhook_dispatch.PLAN_PATH_GLOBSto JSON-routing-files only (plans/**/plan.json+bug-trackerJSON-indexes/plan.json+schemas/v5/**), EXCLUDING plan/content.md, so markdown stays editable; THEN strip theORI_HOOK_DISPATCH_BYPASSenv-bypass (no production consumer — only its own--self-test+test_pretool_hook.py) + the.plan-corpus-bootstrap-activemarker 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_GLOBSnarrowed toplans/**/plan.json+bug-tracker/plans/**/plan.json+schemas/v5/**(plan/content.mdexcluded — markdown stays LLM-editable);.plan-corpus-bootstrap-activemarker check +ORI_HOOK_DISPATCH_BYPASSenv-bypass removed; marker file deleted; §08.3 negative pin added atscripts/plan_corpus/tests/test_block_route_edits.py(test_no_unlock_marker_denies). §08.2status: 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).