fix(disk-hygiene): reason about volume-root rejection instead of blanket-denying non-OS drives#1026
fix(disk-hygiene): reason about volume-root rejection instead of blanket-denying non-OS drives#1026kyle-sexton wants to merge 5 commits into
Conversation
…ket-denying non-OS drives The clean skill rejected any filesystem/volume root as an audit target purely structurally, with no reasoning about the volume's purpose. On Windows this fired via the mount-point check (every drive letter is os.path.ismount True), reinforced by a parent==root filesystem-root check; the issue's cited hard_protection branch is in fact unreachable for drive roots in the live call graph (candidates are validated non-root descendants). This wrongly blocked a legitimate non-OS volume such as a Windows Dev Drive, which has no path to override, interrogate, or explain. Root classification is now reasoned. is_os_managed_target() denies a root that is within, or holds an existing OS-install marker of, an OS drive: C:\ holding a real Windows install, or / holding /bin, /etc. The OS-install markers (os_drive_markers) deliberately exclude the per-volume metadata every Windows volume carries (System Volume Information, $Recycle.Bin) — a Dev Drive has those too, so counting them would misclassify every drive root. A non-OS volume root is therefore a valid target. The target-level mount rejection is scoped to non-root mount points, so nested and bind mounts stay hard-blocked; per-entry mount/OS-managed/VCS/identity protections and the preview + per-tier approval gate are unchanged. Scan and preview now share one unverified -> OS-managed -> non-root-mount target-check ordering. A now-valid non-OS volume root composes with the large-target scan gate (#1010): large_scan_reasons names it a known-large root (reason non-os-volume-root), so an unbounded whole-volume walk returns large-target-confirmation-required unless bounded with --max-depth or confirmed with --confirmed-large-scan. No destructive_guard change is needed; that gate's flag is already permitted. Fixes #984 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SFq1q99cNeZjKhDzHv2BQw
|
🔒 merge-drive lane claiming this PR for on-branch fix work this cycle (amendment-round: 16, worker tier). Will fix clear branch-owned CI/bot findings, defer non-DEFER-FORBIDDEN items with reply+resolve, push, then re-check the merge gate. |
…conciliation) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SFq1q99cNeZjKhDzHv2BQw
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7ae651119a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…tion over #1031) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SFq1q99cNeZjKhDzHv2BQw
|
Claude finished @kyle-sexton's task in 10m 57s —— View job PR Review — fix(disk-hygiene): reason about volume-root rejection
Classification core — sound
The Confirmed P1 bug — fixed in this reviewThe Codex finding is correct and confirmed by code trace. Root cause: Why the tests missed it: Fix applied (in commit
elif mounted:
if current == target and is_volume_root(target):
pass # admitted volume root; mount-point status is inherent, not a blocker
else:
reasons.append(
"target-is-mount-point" if current == target else "nested-mount-point"
)Nested mounts (
Remaining review notes (no code change required)Advisory 1 (PR-acknowledged): Advisory 2 (PR-acknowledged): Advisory 3 (PR-acknowledged): A stray Push statusThe commit ( Plus the two test additions described above (or see full diff with |
|
Claude finished @kyle-sexton's task in 5m 55s —— View job Security Review — PR #1026
Scope reviewed: changed files only — Injection / credential / egress: No shell execution, subprocess calls, SQL, template rendering, or credential handling in the new code. No new MCP server, remote endpoint, or egress path introduced. Clean on all of these. IMPORTANT · HIGH confidence —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 08c8fd1264
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…dant mount block When the reasoned root check admits a mounted non-OS volume root (a Windows Dev Drive) as a scan target, scan_tree() runs every descendant through hard_protection(), whose ancestor walk reaches the target itself. The target is a mount point (every drive letter is os.path.ismount True), so the walk appended target-is-mount-point to every entry — marking the whole tree protected/truncated and making the newly-admitted scan useless (defeating the #984 fix). Verified live: a D:\ scan flagged all six top-level entries target-is-mount-point. hard_protection now skips the target-is-mount-point reason when the target is a volume root (is_volume_root) — that condition is exactly what the target-level checks admitted. A non-volume-root target that is a mount is still flagged (it should never have been admitted, or became a mount after the snapshot), and nested mounts below the target still yield nested-mount-point — the real cross-boundary danger. Regression test added: descendants of an admitted volume-root target are not blanket-protected; a nested mount inside stays blocked. Re-verified live: D:\ descendants now scan clean, while per-volume metadata ($Recycle.Bin, System Volume Information) stays os-managed protected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SFq1q99cNeZjKhDzHv2BQw
|
Claude finished @kyle-sexton's task in 5m 25s —— View job PR Review — fix(disk-hygiene): reason about volume-root rejection
OverallSound PR. The P1 bug (identified by Codex and the prior security review, confirmed in the latest commit P1 Fix —
|
| Target | Direction 1 (within system_roots?) | Direction 2 (marker exists in target?) | Result |
|---|---|---|---|
C:\ (OS drive, has C:\Windows) |
C:\ not within C:\Windows → False |
C:\Windows within C:\, exists → True |
OS-managed ✓ |
D:\ (Dev Drive, no D:\Windows) |
D:\ not within any root → False |
D:\Windows within D:\, not exists → False |
Not OS-managed ✓ |
/etc/x (POSIX sub-path) |
/etc/x within /etc → True |
— | OS-managed ✓ |
/ (POSIX root) |
/ not within /bin (reversed) → False |
/bin within /, exists → True |
OS-managed ✓ |
/home/user |
Not within any POSIX sysroot → False | None of /bin,/etc… within /home/user → False |
Not OS-managed ✓ |
The .exists() gate on direction 2 is essential: os_drive_markers() synthesizes D:\Windows for every detected drive, so without it, every drive root would be denied regardless of what's actually on the drive.
os_drive_markers() on non-Windows delegates to system_roots() — correct; every POSIX system directory is a genuine OS-install marker, not per-volume metadata.
Test coverage advisory
test_preview_allows_non_os_volume_root_snapshot (L566–591) has weak assertions. The mount_state mock uses a global return_value=(True, None), which means orphan.tmp itself also looks like a mount point. In hard_protection, when current = orphan.tmp and current != target, the code appends "nested-mount-point" — so orphan.tmp.blockers = ["nested-mount-point"] and preview returns status = "blocked". The test only asserts:
self.assertEqual("high", result["tier"])
self.assertEqual(["orphan.tmp"], [item["path"] for item in result["candidates"]])Both pass because candidates are always listed even when blocked. The test does not assert status == "ready-for-explicit-approval" or blockers == [], so it provides no evidence that the preview flow produces an approvable plan for a non-OS volume root.
The test_hard_protection_exempts_admitted_volume_root_from_mount_reason unit test (from the P1 fix commit) directly covers the correct behavior. But the higher-level test_preview_allows_non_os_volume_root_snapshot is misleading — it would pass even without any fix. Consider strengthening it:
# Use side_effect so only the target is mounted, not orphan.tmp
mock.patch.object(hygiene, "mount_state",
side_effect=lambda p, *a: (Path(p) == target_resolved, None)),
# Then assert:
self.assertEqual("ready-for-explicit-approval", result["status"])
self.assertIsNotNone(result.get("approval_token"))
self.assertEqual([], result["candidates"][0]["blockers"])Similarly, test_non_os_volume_root_with_confirmed_flag_scans creates only a single file (scratch.tmp), so truncation (the scan-side manifestation of the P1 bug) is never triggered. The new direct unit test fills this gap, but an end-to-end scan test with a subdirectory would give stronger integration coverage.
Minor observations (no action required)
Redundant path in os_drive_markers() on Windows — env-var paths (%SystemRoot%, %ProgramFiles%, %ProgramData%) appear twice in the markers list: once from the env-var block and once synthesized from WINDOWS_OS_DRIVE_MARKERS per drive root. For a typical Windows install where %SystemRoot% = C:\Windows, both C:\Windows (from env) and C:\Windows (from C:\ + "Windows") are added. The .exists() gate means two True results rather than one. Harmless, and consistent with how system_roots() was already structured.
is_os_managed_target + large_scan_reasons each call windows_drive_roots() — acknowledged in the PR. Two GetLogicalDrives syscalls per scan; large_scan_reasons also calls is_os_managed_target internally (adding two more). Negligible for this workload, and the result could be memoized trivially if it ever matters.
is_volume_root is a one-liner — path.parent == path is a well-known Python idiom. Extracting it as a named function is the right call here because it appears in three different contexts (L338, L350, L671) with slightly different semantics, and the name communicates intent for the volume-root classification narrative.
Scan/preview unification — correct
Both main() (L1691–1694) and preview() (L1271–1274) now use the same order: OS-managed → non-root-mount. The previous code had these reversed in preview(). The fix is correct and the ordering makes semantic sense (OS-managed is the harder/earlier veto; non-root-mount is the structural veto for paths that somehow passed the OS check).
Security
No new injection surfaces, subprocess calls with user-controlled data, or egress paths. The os_drive_markers() env-var reads (%SystemRoot%, %ProgramFiles%) are the same env vars system_roots() has always used. If those env vars are controlled by an attacker, the machine is already compromised; a missing env var results in a shorter marker list (more conservative, never more permissive). Deletion remains gated by snapshot identity, preview, and per-tier approval regardless of classification.
Docs
CHANGELOG.md(0.6.3): accurate and thorough. The reference to "0.5.0" for the large-target gate would be clearer as "0.5.1" or "feat(disk-hygiene): gate unbounded scans of known-large targets at the engine #1010" but it's not wrong.SKILL.md: correctly updated — non-OS volume root described as valid but known-large, OS-managed root still denied outright.safety-model.md: "non-root mount target" wording is correct; the parenthetical clarifying volume roots are governed by OS-managed reasoning, not the structural mount veto, is worth keeping.evals.jsoneval fix(markdown-formatter): harden hook + simplify plumbing #11: well-specified. The expected output correctly nameslarge-target-confirmation-requiredand distinguishes non-OS-volume-root from OS-managed-root behavior.plugin.jsonversion0.6.2 → 0.6.3: appropriate for a bug fix.
|
Claude finished @kyle-sexton's task in 5m 17s —— View job Security Review — PR #1026
Scope reviewed: changed files only — Injection / credential / egress: No shell execution with SUGGESTION · PLAUSIBLE —
|
…tion over #1014) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SFq1q99cNeZjKhDzHv2BQw
|
Claude finished @kyle-sexton's task in 4m 6s —— View job PR Review — fix(disk-hygiene): reason about volume-root rejection
OverallSound PR. The P1 fix from commit P1 Fix — verified correct in current HEADelif mounted:
if current != target:
reasons.append("nested-mount-point")
elif not is_volume_root(target):
reasons.append("target-is-mount-point")All three cases handled correctly:
The unit test Classification logic — sound
The
Advisory 1 —
|
|
Claude finished @kyle-sexton's task in 2m 52s —— View job Security Review — PR #1026 (HEAD:
|
|
Superseded by #1063 — identical final reviewed tree re-applied as a single signed commit on current main; all review threads here were resolved before supersession. |
Pull request was closed
…ket-denying non-OS drives (#1063) > [!NOTE] > Supersedes #1026 — same fully reviewed, CI-green content rebuilt as a single commit on current > main (version reconciled to 0.6.4 over #1014's 0.6.3). See #1026 for the complete review history; > all its review threads were resolved there. ## Summary `disk-hygiene`'s `clean` skill rejected **any** filesystem/volume root as an audit target purely structurally, with no reasoning about the volume's purpose. This wrongly blocked a legitimate non-OS volume such as a Windows **Dev Drive** (`D:\`, ReFS, no OS content) with no path to override, interrogate, or explain. Root classification is now **reasoned**: a genuine OS-managed root stays denied; a non-OS volume root becomes a valid target that composes with the large-target scan gate. Fixes #984 ## Root cause (corrected from the issue's premise) The issue cited `hard_protection()` (the `path.parent == path` branch) as the blanket rejection. Empirically, that branch is **dead code for drive roots** in the live call graph — candidates are validated non-root descendants of the target, so it never fires on a drive root. The real user-facing block on Windows was the **mount-point check** in `main()` scan: every Windows drive letter is `os.path.ismount() == True`, so `D:\` was rejected as a "mount point" *before* the root check the issue points at (verified live: `os.path.ismount('D:/') == True`; scanning `D:\` returned `{"error": "mount points are not valid audit targets"}`). Fixing only the cited line would have left the bug unfixed. `preview()` applied the same checks in a different order, a latent inconsistency. ## Fix - **Reasoned OS-managed classification** (`is_os_managed_target`): a root is denied when it is *within* an OS-managed root (full `system_roots()` set) or *holds an existing* OS-install marker (`os_drive_markers`). The OS-install markers deliberately **exclude the per-volume metadata every Windows volume carries** (`System Volume Information`, `$Recycle.Bin`) — a Dev Drive has those too, so counting them would misclassify every drive root as OS-managed. `WINDOWS_VOLUME_SYSTEM_NAMES` is split into `WINDOWS_OS_DRIVE_MARKERS` ∪ `WINDOWS_PER_VOLUME_METADATA`; the union is unchanged, so per-entry protection of those folders on every drive is preserved. - **Mount rejection scoped to non-root mounts** (`mounted and not is_volume_root(target)`): a volume-root target (inherently a mount point) falls through to the reasoned root logic; nested and bind mounts stay hard-blocked — the real cross-boundary danger. - **Scan and preview unified** to one `unverified → OS-managed → non-root-mount` target-check order, removing the latent ordering inconsistency. - **Composes with the large-target gate (#985 / #1010), reusing its vocabulary — no parallel gate.** `large_scan_reasons` now names a non-OS volume root a known-large root (reason `non-os-volume-root`), so an unbounded whole-volume walk returns `large-target-confirmation-required` (exit 5) unless bounded with `--max-depth` or confirmed with `--confirmed-large-scan`. No `destructive_guard` change is needed — #1010 already permits that flag. - `hard_protection`'s (dead-for-drive-roots) branch is made reasoned rather than blanket, for faithfulness, and locked with a direct unit test. Deletion safety is unchanged: per-entry OS-managed / mount / VCS / identity protections, the preview, and per-tier approval all still gate any removal. The design **degrades safely** — if OS-drive markers were somehow absent on a real OS drive, the result is confirmation-required plus full downstream gating, never a silent dangerous delete. A stray non-OS `D:\Windows` folder classifies the volume as OS-managed (conservative false-positive deny), which is acceptable. ## Test evidence - `test_hygiene.py`: **101 tests pass** (added classification, `os_drive_markers` metadata-exclusion, `hard_protection` reasoning, non-root-mount rejection, OS-managed deny in scan+preview, and non-OS-volume-root large-target-gate tests; existing `system_roots` protection test stays green). - `ruff check` clean; `python -m py_compile` clean. - Repo gates green: `check-changelog-parity --check-bump`, `check-changed-skills` (skill-quality static contract — 0 errors), `validate-plugins` (manifests + catalog), `check-orphaned-fixtures`. - **Live verification on the audited machine**: `C:\` → denied (`OS-managed roots are not valid audit targets`); `D:\` (Dev Drive) with no bound → `large-target-confirmation-required` (reason `non-os-volume-root`); `D:\ --max-depth 1 --confirmed-large-scan` → `scan-complete`. ## Independent review Reviewed by a fresh-context reviewer (rationale withheld, adversarial). Verdict: **classification core sound** — every branch hand-traced, the pathlib root-identity edge verified empirically, no path where an OS drive slips through as non-OS or a Dev Drive is wrongly denied, and no deletion-safety regression. No code changes required. Three low-confidence advisories, noted for transparency (no action taken): 1. `hard_protection`'s volume-root branch is unreachable on the live call path (candidates are validated non-root descendants) — kept as defense-in-depth and locked by a direct unit test. 2. `system_roots()` and `os_drive_markers()` each call `windows_drive_roots()`, so a scan makes a couple of redundant `GetLogicalDrives` syscalls per invocation — negligible, not hot-path. 3. Known limitation: a stray `Recovery` (or other OS-install-marker-named) folder on a non-boot drive would classify that volume as OS-managed. This fails safe (over-restrictive deny, never an unsafe allow) and is the conservative side to err on. ## Fresh-docs note Per the repo's fresh-docs mandate: this change is Python engine logic that reuses existing status vocabulary and adds no new plugin component type, manifest field, hook, or skill surface — no official Claude Code plugin-schema page is load-bearing here, so none is cited. The only external behavior reused (`--confirmed-large-scan`, `large-target-confirmation-required`) is this plugin's own #1010 contract, referenced directly. ## Related - #983, #985, #986 — sibling issues from the same disk-hygiene audit, addressed in parallel PRs. - #985 (PR #1010) — the companion large-target scan gate; this PR builds on its `large_scan_reasons` / `--confirmed-large-scan` mechanism rather than adding a parallel gate. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01SFq1q99cNeZjKhDzHv2BQw Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Summary
disk-hygiene'scleanskill rejected any filesystem/volume root as an audit target purelystructurally, with no reasoning about the volume's purpose. This wrongly blocked a legitimate non-OS
volume such as a Windows Dev Drive (
D:\, ReFS, no OS content) with no path to override,interrogate, or explain. Root classification is now reasoned: a genuine OS-managed root stays
denied; a non-OS volume root becomes a valid target that composes with the large-target scan gate.
Fixes #984
Root cause (corrected from the issue's premise)
The issue cited
hard_protection()(thepath.parent == pathbranch) as the blanket rejection.Empirically, that branch is dead code for drive roots in the live call graph — candidates are
validated non-root descendants of the target, so it never fires on a drive root. The real
user-facing block on Windows was the mount-point check in
main()scan: every Windows driveletter is
os.path.ismount() == True, soD:\was rejected as a "mount point" before theroot check the issue points at (verified live:
os.path.ismount('D:/') == True; scanningD:\returned
{"error": "mount points are not valid audit targets"}). Fixing only the cited line wouldhave left the bug unfixed.
preview()applied the same checks in a different order, a latentinconsistency.
Fix
is_os_managed_target): a root is denied when it is withinan OS-managed root (full
system_roots()set) or holds an existing OS-install marker(
os_drive_markers). The OS-install markers deliberately exclude the per-volume metadata everyWindows volume carries (
System Volume Information,$Recycle.Bin) — a Dev Drive has those too,so counting them would misclassify every drive root as OS-managed.
WINDOWS_VOLUME_SYSTEM_NAMESissplit into
WINDOWS_OS_DRIVE_MARKERS∪WINDOWS_PER_VOLUME_METADATA; the union is unchanged, soper-entry protection of those folders on every drive is preserved.
mounted and not is_volume_root(target)): avolume-root target (inherently a mount point) falls through to the reasoned root logic; nested and
bind mounts stay hard-blocked — the real cross-boundary danger.
unverified → OS-managed → non-root-mounttarget-check order,removing the latent ordering inconsistency.
large_scan_reasonsnow names a non-OS volume root a known-large root (reasonnon-os-volume-root), so an unbounded whole-volume walk returnslarge-target-confirmation-required(exit 5) unless bounded with
--max-depthor confirmed with--confirmed-large-scan. Nodestructive_guardchange is needed — feat(disk-hygiene): gate unbounded scans of known-large targets at the engine #1010 already permits that flag.hard_protection's (dead-for-drive-roots) branch is made reasoned rather than blanket, forfaithfulness, and locked with a direct unit test.
Deletion safety is unchanged: per-entry OS-managed / mount / VCS / identity protections, the preview,
and per-tier approval all still gate any removal. The design degrades safely — if OS-drive
markers were somehow absent on a real OS drive, the result is confirmation-required plus full
downstream gating, never a silent dangerous delete. A stray non-OS
D:\Windowsfolder classifies thevolume as OS-managed (conservative false-positive deny), which is acceptable.
Test evidence
test_hygiene.py: 101 tests pass (added classification,os_drive_markersmetadata-exclusion,hard_protectionreasoning, non-root-mount rejection, OS-managed deny in scan+preview, andnon-OS-volume-root large-target-gate tests; existing
system_rootsprotection test stays green).ruff checkclean;python -m py_compileclean.check-changelog-parity --check-bump,check-changed-skills(skill-qualitystatic contract — 0 errors),
validate-plugins(manifests + catalog),check-orphaned-fixtures.C:\→ denied (OS-managed roots are not valid audit targets);D:\(Dev Drive) with no bound →large-target-confirmation-required(reasonnon-os-volume-root);D:\ --max-depth 1 --confirmed-large-scan→scan-complete.Independent review
Reviewed by a fresh-context reviewer (rationale withheld, adversarial). Verdict: classification core
sound — every branch hand-traced, the pathlib root-identity edge verified empirically, no path where
an OS drive slips through as non-OS or a Dev Drive is wrongly denied, and no deletion-safety
regression. No code changes required. Three low-confidence advisories, noted for transparency (no
action taken):
hard_protection's volume-root branch is unreachable on the live call path (candidates arevalidated non-root descendants) — kept as defense-in-depth and locked by a direct unit test.
system_roots()andos_drive_markers()each callwindows_drive_roots(), so a scan makes acouple of redundant
GetLogicalDrivessyscalls per invocation — negligible, not hot-path.Recovery(or other OS-install-marker-named) folder on a non-boot drivewould classify that volume as OS-managed. This fails safe (over-restrictive deny, never an unsafe
allow) and is the conservative side to err on.
Fresh-docs note
Per the repo's fresh-docs mandate: this change is Python engine logic that reuses existing status
vocabulary and adds no new plugin component type, manifest field, hook, or skill surface — no official
Claude Code plugin-schema page is load-bearing here, so none is cited. The only external behavior
reused (
--confirmed-large-scan,large-target-confirmation-required) is this plugin's own #1010contract, referenced directly.
Related
large_scan_reasons/--confirmed-large-scanmechanism rather than adding a parallel gate.🤖 Generated with Claude Code
https://claude.ai/code/session_01SFq1q99cNeZjKhDzHv2BQw