feat(rfc-008): P4d S5 — install.mjs --uninstall-enforcement [--purge-config]#416
Merged
Merged
Conversation
…config] Reverse the per-project enforcement install delta: prune the 9 enforcement hook registrations and delete the enforcement-only files (gates + engine + classifier + markers + the hooks/lib/*.sh closure) and the enforcement-only hooks/patterns + hooks/plugins dirs, while PRESERVING the core bp1 set, the operator-owned enforce-config.json (unless --purge-config), and the global substrate. Never touches ~/.claude or ~/.episodic-memory. Correctness proof = the REQ-12 core-state delta E2E: in ONE mock project, core-install -> snapshot -> --install-enforcement -> --uninstall-enforcement -> re-snapshot deep-equals the core baseline (tree + parsed settings). Auto-catches over-removal (bp1), under-removal (.sh libs), and residual empty event keys / patterns+plugins dirs without trusting the hand-derived removal list. One new manifest export (enforcementHookLibBasenames), readdir-coupled to install's 5_lib deploy so the deploy and removal sets agree by construction (Rule 14). Reviewed via second-opinion (claude-subagent), overall ACCEPT, 0 blocking. Findings dispositioned: F1 (operator command bundled in the same hooks[] entry as a gate was collaterally deleted by whole-entry removal) FIXED inline with a command-granularity prune + regression test; F2 .sh-lib drift guard added as a co-located machine check; F3/F6 help-text clarifications; F4 explicit bp1 H1 assertion; F5 (version-skew) doc note + tracking issue. Tests: tests/test-uninstall-enforcement.mjs 14/14 (incl. core-state delta, containment fail-closed, malformed-settings atomicity, F1 shared-entry, F2 drift guard). Regression green: contract-deploy 36, lib-closure 6, activation-scoping 13, enforcement-scope 17, s4-gate-e2e 5, bp1-wiring 23. RFC-008 P4d S5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lantiscooperdev
left a comment
Collaborator
There was a problem hiding this comment.
Code review — RFC-008 P4d S5 (--uninstall-enforcement)
Reviewed the removal-set algebra, registration prune, containment, atomicity, and the test coverage against the diff and the live install/uninstall flow.
Verified sound
- Removal algebra —
rootRemove/libRemove= (enforcement sets) MINUS (core bp1 sets), placed in the correct subdir (root vslib/); enforcement-onlypatterns/+plugins/removed wholesale (core never creates them). bp1.mjs,bp1-approval-check.sh,bp1-sweep-on-session.sh, and bp1-shared.mjslibs are correctly excluded. - Registration prune — canonical exact-match strings are byte-identical to what install writes (
shellQuote(abs)for gates,node <quoted abs>for SessionEnd); stop-gate's Stop + SubagentStop both handled; legacy-relative spellings resolved + removed; foreign same-basename hooks warned and left. - Containment —
assertContainedrealpaths both sides and usespath.relative, so symlink-escape and the.claude/hooks-backup/sibling-prefix both fail closed with the escape target intact. - Atomicity — parse-first;
SyntaxErroraborts before any delete/write; settings written before file deletion, so a containment throw never orphans registrations against deleted files. - No global touch — all paths rooted at
projectDir; the before/after global snapshot is byte-identical.
Findings dispositioned (from the second-opinion claude-subagent pass)
- F1 (medium): whole-entry
removeHookEntryByCommandwould collaterally delete an operator command bundled in the samehooks[]entry as a gate (reproduced on disk). Fixed inline here via the command-granularityremoveHookCommandFromEvent(drops an entry only when it empties it), covered byt_operator_command_in_shared_entry_preserved. Good resolution — keeps the shared install-time helper's whole-entry semantics intact. - F2 → co-located drift guard
t_sh_libs_not_sourced_by_core(Rule 14). F4 → explicit bp1 H1 assertion. F3/F6 → help-text. F5/OD-2 → #414 / #415.
Tests
test-uninstall-enforcement.mjs 14/14 incl. the core+enforce+uninstall ≡ core delta; regression suites green (contract-deploy/lib-closure/activation-scoping/enforcement-scope/s4-gate-e2e/bp1-wiring).
No blocking issues. The core-state delta E2E is the right correctness anchor and it holds. Recommend merge after maintainer approval.
lantiscooperdev
approved these changes
Jun 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RFC-008 P4d S5 — per-project enforcement uninstall
Adds
install.mjs --uninstall-enforcement [--purge-config], reversing the per-project enforcement install delta (RFC-008 P4d, decouple enforcement from the substrate).What it does
<project>/.claude/settings.json(canonical exact-match + legacy relative spellings; foreign same-basename hooks are warned and left — REQ-5).hooks/lib/*.shclosure, and the enforcement-onlyhooks/patterns+hooks/pluginsdirs (wholesale — core never creates them).bp1-*.mjs+ closure libs +bp1-approval-check.shH1 +bp1-sweep-on-session.shH2), the operator-ownedenforce-config.json(unless--purge-config), and the global substrate (~/.claude,~/.episodic-memory).Correctness model
Primary proof is the single-mock core-state delta E2E: in ONE project, core-install → snapshot →
--install-enforcement→--uninstall-enforcement→ re-snapshot deep-equals the core baseline (.claudetree + parsed settings). It auto-catches over-removal (bp1), under-removal (the.shlibs), and residual empty event keys /patterns+pluginsdirs without trusting the hand-derived removal list.One new manifest export (
enforcementHookLibBasenames) is readdir-coupled to install's5_libdeploy so the deploy and removal sets agree by construction (Rule 14).Safety
assertContained(realpath both sides +path.relative, not barestartsWith) fails closed on a symlinked hook/dir escaping the project tree, including the sibling-prefix case (.claude/hooks-backup/).settings.json: parse first; onSyntaxErrorthe whole op aborts (no delete, no write).Review (Rule 18)
--provider claude-subagent): overall ACCEPT, 0 blocking. Findings dispositioned — F1 (operator command bundled in the samehooks[]entry as a gate was collaterally deleted by whole-entry removal; reproduced on disk) fixed inline with a command-granularity prune + regression test; F2.sh-lib drift guard added as a co-located machine check; F3/F6 help-text clarifications; F4 explicit bp1 H1 assertion; F5 version-skew → RFC-008 P4d: uninstall-enforcement version-skew under-removal (review F5) #414; OD-2--uninstall-second-opinion→ RFC-008 P4d: add --uninstall-second-opinion (OD-2) #415.Tests
tests/test-uninstall-enforcement.mjs— 14/14 (core-state delta, registrations removed, files removed, bp1 preserved, switch preserved, purge, no-global-touch, foreign-hook preserved, idempotent, containment fail-closed, malformed-settings atomic, cwd-independent, F1 shared-entry, F2 drift guard).Follow-ups: #414 (F5 version-skew), #415 (OD-2). OD-3 (branch-protection required-check) lands with S8.
🤖 Generated with Claude Code