Skip to content

docs(disk-hygiene): disclose the PowerShell lane's uncovered mutation spellings - #1305

Merged
kyle-sexton merged 4 commits into
mainfrom
docs/386-disk-hygiene-doc-corrections
Jul 25, 2026
Merged

docs(disk-hygiene): disclose the PowerShell lane's uncovered mutation spellings#1305
kyle-sexton merged 4 commits into
mainfrom
docs/386-disk-hygiene-doc-corrections

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Closes #386

Summary

#386 grouped three disk-hygiene doc corrections (D1, D2, D4 — the body has no D3). Re-verified each
against current main before implementing: two of the three were already fixed by later work, so
this PR lands only the one that is still live.

D1 — Windows mislabeled "full": already fixed. skills/setup/SKILL.md now splits the lanes
explicitly ("Windows (full audit … engine execution unsupported"), matching
execution_blockers in hygiene.py. README.md says the same. No change needed.

D2 — false "hook process receives CLAUDE_PLUGIN_DATA" premise: already fixed. Both cited sites
now describe the real mechanism — clean/SKILL.md ("validates --data-root against the plugin data
directory it derives from ${CLAUDE_PLUGIN_ROOT}") and reference/safety-model.md (derivation +
--authorized-data-root + env fallback, with the fail-closed consequence stated). No change needed.
The 0.3.0 CHANGELOG entry still carries the original wording and is deliberately left alone: it is
a released historical record, the issue scoped it out ("Beyond the already-known CHANGELOG 0.3.0
line"), and later entries (0.4.x, 0.9.0) already record the correction.

D4 — PowerShell-lane gaps: half fixed, half live. Sub-gap (a), the kill switch not closing the
lane, was fixed by #382 — the guard now denies flagged spellings outright in audit-only mode, and the
docs say so. Sub-gap (b) is still real: the lane enumerates spellings, and Move-Item/mv,
Rename-Item, overwriting writers (Set-Content, Out-File, >, New-Item -Force), and
Format-Volume/Clear-Disk are absent from _POWERSHELL_MUTATION_WORDS, so they reach the tool with
no guard verdict at all. The docs described the lane's coverage without naming that hole, which
reads as broader protection than the regex delivers.

This PR discloses that hole once, where the security model is stated
(reference/safety-model.md), names the consumer permission policy as its only backstop, and carries
TODO(#387). clean/SKILL.md's existing "raised bar, not a fail-closed lane" gotcha gains one clause
pointing at that paragraph rather than restating it.

Review-driven follow-ups (Codex, both accepted) tightened two adjacent over-promises the disclosure
exposed: the fail-closed paragraph's "a human prompt before every mutation" is now bounded to what the
guard sees, the audit-only "denies every mutation lane" to the two lanes it gates, and the manual
handoff's per-path approval is no longer offered as a backstop here — it approves the paths selected
for removal, so it cannot cover what an unflagged spelling collaterally destroys (a Move-Item -Force
destination, a truncated Out-File target, a whole volume).

Docs only — no guard behavior changes. Extending the flagged set is #387's job, not this issue's.

Test plan

  • markdownlint-cli2 over plugins/disk-hygiene/**/*.md — 0 errors.
  • scripts/check-changed-skills.sh origin/mainclean PASS, 0 errors (344/500 lines, all 6 base-ref
    trigger phrases preserved).
  • scripts/check-skill-portability.sh origin/main — no unexcused coupling tokens.
  • scripts/validate-plugins.sh — all manifests + catalog valid.
  • scripts/check-changelog-parity.sh --check-bump origin/main — version bump 0.9.0 → 0.9.1 has its
    matching CHANGELOG entry.

Fresh-docs mandate discharged for the harness-behavior claims sitting at the edit site:
plugins-reference (environment variables +
persistent data directory) and hooks (skill-frontmatter
hooks). Result: the reference documents all three path variables as exported to hook processes and
substituted in "hook and monitor commands", but neither page carves out skill-frontmatter hooks,
so the existing safety-model text — which already attributes the non-export to an observed Claude Code
build rather than to the docs — is still accurate and was left unchanged.

Related

🤖 Generated with Claude Code

… spellings

The safety model and the clean skill described the PowerShell belt as gating
"known deletion spellings" without stating what it does not see. Move, rename,
overwriting-writer, and volume-format spellings reach the tool with no guard
verdict at all — audit-only mode included — so the documented protection read
broader than the enumerated regex delivers.

Name the uncovered families where the security model is stated, with the
per-path human approval and the consumer permission policy identified as the
only backstops there. Guard behavior is unchanged; closing the gap is #387.

Closes #386

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 37s —— View job


PR Review

  • Gather context and understand the request
  • Read PR diff and changed files
  • Review for correctness, security, and alignment with CLAUDE.md guidelines
  • Post findings

This is a clean, well-scoped docs-only PR. The content is accurate and the disclosure is appropriately placed. Two minor notes:

Nit: TODO(#387) renders inline in its paragraph

reference/safety-model.md L154 — the TODO line has no blank line before it, so Markdown will render it as the last sentence of the preceding paragraph:

"…and the consumer's own permission policy, never by this guard. TODO(#387): extend the flagged set to those spellings."

That reads naturally enough, but if the intent is to set it apart from the security-model prose (so it reads as a tracking annotation, not a normative claim), adding a blank line before it or converting it to a blockquote would separate it visually. Not a blocker.

Observation: CHANGELOG entry uses ### Changed, not ### Fixed

Correct choice — this is a documentation disclosure, not a behavior fix. Consistent with the project's prior entries and the Keep a Changelog spec.


Everything else looks good:

  • The disclosed spellings are accurate: Move-Item/mv (PowerShell alias), Rename-Item, Set-Content, Out-File, >, New-Item -Force, Format-Volume, Clear-Disk are all plausibly absent from a deletion-shaped enumeration. The disclosure doesn't over-claim or under-claim the gap.
  • The backstop attribution ("per-path human approval … and the consumer's own permission policy, never by this guard") is precise — it doesn't imply the gap is mitigated by the guard when it isn't.
  • The SKILL.md cross-reference (reference/safety-model.md) avoids duplicating the full list, which is the right call; the gotcha entry stays a summary pointer.
  • Version bump 0.9.0 → 0.9.1 is correct for a patch-level docs change.
  • The PR's own claim that D1 and D2 were already closed is verifiable from the files; no changes to those sites is the correct action.
  • The deliberate non-touch of the 0.3.0 CHANGELOG entry is justified — historical records should not be retroactively edited.

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 49s —— View job


Security Review

Tasks

  • Gather context and understand the PR
  • Fetch PR diff
  • Read changed files
  • Analyze for security vulnerabilities
  • Post findings

Scope

Four files changed:

  • plugins/disk-hygiene/.claude-plugin/plugin.json — version bump only
  • plugins/disk-hygiene/CHANGELOG.md — changelog entry
  • plugins/disk-hygiene/skills/clean/SKILL.md — gotcha clause updated
  • plugins/disk-hygiene/skills/clean/reference/safety-model.md — disclosure paragraph added

All changes are documentation only. No Python guard logic, hook configuration, permission grants, workflow files, or dependency pins are touched.


Findings

No security issues found.

The substantive change is the new paragraph in safety-model.md lines 147–154 and the matching clause in SKILL.md lines 338–341. Both accurately describe a pre-existing gap in the _POWERSHELL_MUTATION_WORDS enumeration (non-deletion destructive spellings pass through ungated). This is a disclosure, not a regression — the gap predates this PR, the remaining backstops (per-path human approval and consumer permission policy) are correctly named, and the fix is tracked in #387.

Nothing in the diff:

  • introduces a new execution surface or injection vector
  • widens permissions or trust delegation
  • modifies GitHub Actions workflows
  • loosens dependency or action pins
  • exposes secrets or credentials

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78a95d3f76

ℹ️ 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".

Comment thread plugins/disk-hygiene/skills/clean/reference/safety-model.md Outdated
…ed spellings

Codex review on #1305: the fail-closed paragraph promised "a human prompt
before every mutation", which the newly disclosed PowerShell coverage hole
contradicts — an unflagged Move-Item or Set-Content defers with no verdict
whether the toggle is readable or not. Bound the guarantee to what the guard
actually sees, and separate the TODO annotation from the security-model prose
so it does not read as a normative claim.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 26s —— View job


PR Review

  • Gather context and understand the request
  • Read PR diff and changed files (HEAD: dbfa4e2)
  • Assess Codex P2 finding vs. latest commit
  • Post findings

The second commit (dbfa4e2) directly closes the Codex P2 finding before this review ran.

Codex P2 — "Reconcile the all-mutations guarantee": addressed

The disputed sentence at safety-model.md L180–183 now reads:

"…the guard stays active and forces a human prompt before every mutation it sees — every Bash engine apply, and on PowerShell only the flagged spellings above — so an unreadable toggle never silently disables the guard."

The qualification "it sees" plus the parenthetical breakdown eliminates the contradiction: the guarantee is now scoped to what the guard actually observes, not to all possible mutations. The Codex finding is resolved without weakening the fail-closed claim for the surfaces the guard does cover.

Prior Claude review nit — TODO(#387) rendering: resolved

safety-model.md L155 now has blank lines before and after the TODO line, so it renders as a standalone paragraph rather than the last sentence of the body prose. The previous nit no longer applies.


Everything else remains clean:

  • Disclosure paragraph (L147–155): accurate enumeration of uncovered spellings, precise backstop attribution ("per-path human approval … and the consumer's own permission policy, never by this guard"), and a tracked TODO. No over- or under-claim.
  • SKILL.md gotcha (L337–340): summary pointer to reference/safety-model.md avoids duplicating the full list; the cross-reference is the right call.
  • CHANGELOG 0.9.1: ### Changed is correct for a docs-only disclosure. Entry text is accurate.
  • plugin.json: patch bump 0.9.0 → 0.9.1 is appropriate.
  • Docs-only; no guard logic, hook config, workflow files, or permission grants touched.

No outstanding issues. Ready to merge.

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 2s —— View job


Security Review

Tasks

  • Gather context and understand the PR
  • Fetch PR diff
  • Read changed files
  • Analyze for security vulnerabilities
  • Post findings

Scope

Four files changed:

  • plugins/disk-hygiene/.claude-plugin/plugin.json — version bump only (0.9.0 → 0.9.1)
  • plugins/disk-hygiene/CHANGELOG.md — changelog entry
  • plugins/disk-hygiene/skills/clean/SKILL.md — one gotcha clause updated
  • plugins/disk-hygiene/skills/clean/reference/safety-model.md — disclosure paragraph added; fail-closed sentence qualified

All changes are documentation only. No guard logic, hook configuration, permission grants, workflow files, or dependency pins are touched.


Findings

No security issues found.

The substantive changes are:

  1. New disclosure paragraph (safety-model.md lines 147–155) — names the ungated spellings (Move-Item/mv, Rename-Item, Set-Content, Out-File, >, New-Item -Force, Format-Volume, Clear-Disk) and their backstops. Accurate; improves security-posture transparency.

  2. Qualified fail-closed sentence (safety-model.md lines 180–183) — adds "it sees" and enumerates scope: "every Bash engine apply, and on PowerShell only the flagged spellings above." This directly addresses the Codex review's comment about the previous absolute "every mutation" guarantee being too broad — that finding is already closed by this diff.

Nothing in the diff:

  • Introduces an execution surface or injection vector
  • Widens permissions or trust delegation
  • Modifies GitHub Actions workflows
  • Loosens dependency or action pins
  • Exposes secrets or credentials
  • Changes any runtime behavior (guard logic is untouched)

The gap disclosed (unflagged PowerShell spellings) pre-dates this PR. This PR makes the documentation more accurate about that pre-existing gap, which is a net improvement to security posture.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbfa4e2f2c

ℹ️ 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".

Comment thread plugins/disk-hygiene/skills/clean/reference/safety-model.md Outdated
Same over-promise Codex caught in the fail-closed paragraph, in the audit-only
direction: "denies every mutation lane" reads as total coverage, but the belt
only ever sees the flagged deletion spellings. Name the two lanes as the scope
rather than implying all of them.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 49s —— View job


PR Review

  • Gather context and understand the request
  • Read PR diff and changed files (HEAD: 111545d)
  • Assess Codex P2 finding vs. latest commit
  • Post findings

111545d (the current HEAD) changes only SKILL.md — it does not touch safety-model.md. Here's what it does and what it doesn't:

What 111545d fixes

SKILL.md L54 — audit-only lane claim tightened. The sentence previously read "it denies every mutation lane outright"; it now reads "it denies both mutation lanes it gates outright." This correctly scopes the audit-only guarantee to what the guard actually observes, matching the qualified guarantee added in dbfa4e2 (safety-model.md L181-183).

SKILL.md L339-341 — gotcha clause expanded. The previous version said only "It is a raised bar, not a fail-closed lane." The new version names the uncovered spellings inline — "move, rename, overwrite, and volume-format spellings are not flagged at all (reference/safety-model.md)" — and cross-references the disclosure paragraph. This is the right shape for a gotcha: one sentence pointing to the authoritative site.

Remaining concern from Codex P2 (second round, on dbfa4e2)

safety-model.md L152-153 is unchanged from dbfa4e2:

"Data loss through those spellings is held only by the per-path human approval the manual handoff already requires and the consumer's own permission policy, never by this guard."

Codex flagged that Format-Volume/Clear-Disk operate at volume scope rather than path scope, so the manual-handoff's per-path approval does not backstop them. That critique is accurate: the handoff confirms cleanup-path-relative operations; a Format-Volume C: call isn't gated by which paths were selected for removal. If the consumer's permission policy also pre-allows the call silently, neither backstop applies.

This is not a regression — the gap predates the PR, and the paragraph already says "no guard verdict at all, in audit-only mode included." The over-broad backstop claim is a precision issue in the disclosure, not an omission of the gap itself. But since the Codex P2 called this out explicitly and 111545d doesn't address it, it remains open.

A minimal fix would qualify the per-path approval claim to path-scoped spellings — something like: "Data loss through path-scoped spellings (Move-Item, Rename-Item, overwriting writers) is constrained by the per-path human approval the manual handoff requires; volume-level operations (Format-Volume, Clear-Disk) have no path-scope backstop beyond the consumer's own permission policy." Fix this →

Everything else

  • SKILL.md L54 and L339-341 changes are correct and well-scoped.
  • CHANGELOG 0.9.1 ### Changed entry is accurate.
  • Version bump 0.9.0 → 0.9.1 is appropriate for a patch-level docs change.
  • Docs-only — no guard logic, hook config, workflow files, or permission grants touched.

Bottom line: Three of the four concerns raised across this review cycle are fully addressed. The one remaining item is the safety-model.md backstop claim for volume-level ops — whether to address it here or carry it into #387 is the author's call; the PR's net effect is still a clear improvement in disclosure accuracy.

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 42s —— View job


Security Review

Tasks

  • Gather context and understand the PR
  • Fetch PR diff
  • Read changed files
  • Analyze for security vulnerabilities
  • Post findings

Scope

Four files changed across three commits (78a95d3dbfa4e2111545d):

  • plugins/disk-hygiene/.claude-plugin/plugin.json — version bump 0.9.0 → 0.9.1 only
  • plugins/disk-hygiene/CHANGELOG.md — changelog entry
  • plugins/disk-hygiene/skills/clean/SKILL.md — two gotcha-clause edits (78a95d3 and 111545d)
  • plugins/disk-hygiene/skills/clean/reference/safety-model.md — disclosure paragraph added; fail-closed sentence scoped (78a95d3 and dbfa4e2)

All changes are documentation only. No guard Python logic, hook configuration, permission grants, workflow files, dependency pins, or secrets are touched.


Findings

SUGGESTION · PLAUSIBLE — Backstop attribution overstates containment for volume operations

safety-model.md L152–153

"Data loss through those spellings is held only by the per-path human approval the manual handoff already requires and the consumer's own permission policy, never by this guard."

The sentence attributes "per-path human approval" as a backstop for every listed spelling uniformly, but Format-Volume and Clear-Disk operate at volume granularity — they have no snapshot-relative paths for the manual handoff's per-path gate to evaluate. If a consumer's permission policy allows the PowerShell tool and one of those volume commands is issued, the only real backstop is the policy alone; the per-path approval pathway has no surface to fire. The sentence as written implies bilateral containment (human approval + policy) for all listed spellings, when volume operations have only unilateral containment (policy only).

This is a documentation accuracy issue, not a code regression; the underlying gap is tracked in #387. But a user reading this disclosure to decide whether to enable the lane could infer stronger backstop coverage for Format-Volume/Clear-Disk than actually exists. Qualifying the "per-path approval" clause — e.g. "the per-path human approval the manual handoff requires where path-level granularity applies" — or separating volume operations into their own sentence ("volume operations such as Format-Volume/Clear-Disk have no path-level gate; only the consumer's permission policy applies") would close the over-claim.

This is the live Codex P2 finding raised against dbfa4e2 that was not addressed by the HEAD commit 111545d (which only touched SKILL.md).


Everything else is clean:

  • No execution surface, injection vector, or permission-widening change is introduced.
  • No GitHub Actions workflows, action pins, or dependency pins are modified.
  • No secrets or credentials are present.
  • The fail-closed sentence qualification at L180–183 (added in dbfa4e2) correctly bounds the "human prompt before every mutation it sees" guarantee to the guard's actual observation surface; the first Codex P2 finding is resolved.
  • The SKILL.md edit at L337–340 and the audit-only scope qualification (111545d) accurately scope the deny surface to the belted spellings; no over-claim remains there.

…or these spellings

Codex review on #1305: the manual handoff approves the paths selected for
removal, so it cannot cover what an unflagged mutation collaterally destroys —
a Move-Item -Force destination, a truncated Out-File target, a whole volume.
Listing it as a backstop overstated the residual protection in the same way the
paragraph set out to stop. The consumer's permission policy is the only one
left.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 12s —— View job


PR Review

  • Gather context and understand the request
  • Read changed files (HEAD: d78e6ac)
  • Assess whether the remaining Codex P2 finding is addressed
  • Post findings

d78e6ac closes the last open item from this review cycle.

Codex P2 (second round) — "Do not treat path approval as containment for these mutations": resolved

safety-model.md L152–155 now reads:

"The only thing standing between them and the filesystem is the consumer's own permission policy, never this guard: the manual handoff's per-path approval covers the paths selected for removal, so it does not reach what such a command collaterally destroys — a Move-Item -Force destination, a truncated Out-File target, or an entire volume."

The previous wording listed the per-path approval as a co-backstop alongside the permission policy; this commit correctly strips it out and instead explains why it doesn't apply — the handoff's approval is scoped to removal paths, not to collateral targets. The concrete examples (Move-Item -Force destination, truncated Out-File target, entire volume) make the gap legible to the reader and match the three categories enumerated in the disclosure. The CHANGELOG update (0.9.1 ### Changed) tracks the same correction accurately.


All findings across this review cycle are now resolved:

Finding Raised in Commit
TODO(#387) standalone paragraph rendering Claude review round 1 dbfa4e2
Fail-closed "every mutation" guarantee contradicts disclosure Codex P2 round 1 dbfa4e2
Audit-only lane scope in SKILL.md L54 Claude review round 3 111545d
SKILL.md gotcha clause cross-reference Claude review round 3 111545d
Per-path approval overstated as backstop for volume ops Codex P2 round 2 / Claude review round 4 d78e6ac

No outstanding issues. Ready to merge.

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 59s —— View job


Security Review

Tasks

  • Gather context and understand the PR
  • Fetch PR diff (HEAD: d78e6ac)
  • Read changed files
  • Analyze for security vulnerabilities
  • Post findings

Scope

Four files changed across four commits (78a95d3dbfa4e2111545dd78e6ac):

  • plugins/disk-hygiene/.claude-plugin/plugin.json — version bump 0.9.0 → 0.9.1 only
  • plugins/disk-hygiene/CHANGELOG.md — changelog entry (docs)
  • plugins/disk-hygiene/skills/clean/SKILL.md — two gotcha-clause edits (docs)
  • plugins/disk-hygiene/skills/clean/reference/safety-model.md — disclosure paragraph added; fail-closed sentence scoped; per-path backstop qualified (docs)

All changes are documentation only. No guard Python logic, hook configuration, permission grants, workflow files, dependency pins, or secrets are touched.


Findings

No security issues found.

The live Codex P2 finding that survived the 111545d review round — "per-path approval presented as a backstop for volume operations" — is directly closed by HEAD commit d78e6ac.

The paragraph at safety-model.md L147–155 now reads:

"The only thing standing between them and the filesystem is the consumer's own permission policy, never this guard: the manual handoff's per-path approval covers the paths selected for removal, so it does not reach what such a command collaterally destroys — a Move-Item -Force destination, a truncated Out-File target, or an entire volume."

This is precise: it names the sole backstop (permission policy), explicitly excludes the guard, and explains why per-path approval doesn't apply to these spellings rather than implying it does. The volume-operation case (Format-Volume/Clear-Disk) is now correctly described as having only unilateral containment (policy only), not bilateral.

All other findings from the review cycle remain closed:

  • Codex P2 (on 78a95d3) — "Reconcile all-mutations guarantee": Closed by dbfa4e2 — fail-closed sentence now bounded to "every mutation it sees" with an explicit scope breakdown.
  • Claude nit (on 78a95d3) — TODO renders inline: Closed by dbfa4e2 — blank lines added before and after the TODO paragraph.
  • Claude observation (on 111545d) — audit-only scope overstatement in SKILL.md: Closed by 111545d — "denies every mutation lane" → "denies both mutation lanes it gates."
  • Codex P2 (on dbfa4e2) — per-path approval overstated for volume ops: Closed by d78e6ac (this HEAD) as described above.

Nothing in the diff:

  • Introduces an execution surface or injection vector
  • Widens permissions or trust delegation
  • Modifies GitHub Actions workflows
  • Loosens dependency or action pins
  • Exposes secrets or credentials
  • Changes any runtime behavior (guard logic untouched)

All prior security findings resolved. No new issues introduced.

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Closing out the one item from the security review on 111545d1 that was still open at the time it was written.

# Finding Classification Evidence Reacted
1 Security review on 111545d1 — backstop attribution overstates containment for volume operations (safety-model.md L152-153) VALID — already addressed on HEAD Closed by d78e6ac5, which drops per-path approval from the backstop list entirely rather than qualifying it. HEAD now reads: "The only thing standing between them and the filesystem is the consumer's own permission policy, never this guard: the manual handoff's per-path approval covers the paths selected for removal, so it does not reach what such a command collaterally destroys — a Move-Item -Force destination, a truncated Out-File target, or an entire volume." The follow-up security review on d78e6ac5 independently reports the same item closed. 👍

Verified against the guard source rather than the review prose: powershell_decision in plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py returns a verdict only for _POWERSHELL_MUTATION_WORDS (remove-item|rm|rmdir|del|erase|rd|ri|clear-content|clear-recyclebin|rimraf|unlink|sendtorecyclebin|deletefile|deletedirectory|removedirectory), _POWERSHELL_DOTNET_DELETE, _POWERSHELL_QUALIFIED_DELETE, and _POWERSHELL_ROBOCOPY_PURGE; every other command falls through to return None. Move-Item/mv, Rename-Item, Set-Content, Out-File, >, New-Item -Force, Format-Volume, and Clear-Disk are absent from all four patterns, so the disclosure this PR adds is accurate and the corrected backstop attribution matches the code.

Both review threads on this PR are addressed on HEAD d78e6ac5; no further branch change is needed.

This was generated by AI on operator direction. 🤖

@kyle-sexton
kyle-sexton merged commit 6d51e05 into main Jul 25, 2026
28 checks passed
@kyle-sexton
kyle-sexton deleted the docs/386-disk-hygiene-doc-corrections branch July 25, 2026 16:46
kyle-sexton added a commit that referenced this pull request Jul 25, 2026
… setup check (#1124)

## Summary

Closes a fail-open vector in the `disk-hygiene` destructive-action guard
on Windows. The `clean` skill registers its PreToolUse guard as the
literal hook command `python3` (`skills/clean/SKILL.md`). On stock
Windows, `python3` resolves to
`%LOCALAPPDATA%\Microsoft\WindowsApps\python3.exe` — a zero-length App
Execution Alias reparse stub that opens the Microsoft Store (or exits)
instead of running an interpreter. The guard process never starts, so it
emits neither of the two signals that let a PreToolUse hook block a tool
call — exit code 2 or a `deny` decision — and Claude Code lets the
destructive `Bash`/`PowerShell` command proceed **ungated**. This is the
same fail-open shape as the 0.6.3 fix (a failed hook launch treated as
non-blocking), reached through a new vector: the guard's launch *name*
resolving to the Store stub rather than a real interpreter.

This is a `DEFER-FORBIDDEN` fail-open gap in a safety guard — detection,
tests, and docs are all part of the fix, not follow-ups.

## Fix

- **New inspect-only probe**
`skills/setup/scripts/python3_alias_probe.py`: classifies what the name
`python3` resolves to (via `shutil.which`, `--path` override for tests)
**without executing it** (running the stub pops the Store / hangs).
Verdicts: `store-alias-stub` (zero-length file under a `WindowsApps`
path component), `ok` (real interpreter — including the Store's
*genuine* Python under a versioned
`WindowsApps\PythonSoftwareFoundation...\` subdir, which has non-zero
size), `not-found`, `indeterminate` (identity unreadable). On Windows it
also reports the `IO_REPARSE_TAG_APPEXECLINK` reparse attribute as
corroborating evidence, but the cross-platform verdict uses the portable
zero-length + `WindowsApps`-component signal.
- **`setup check` (SKILL.md step 1)** now runs the probe after locating
the working interpreter and **fails closed on every verdict except
`ok`**: `store-alias-stub` and `indeterminate` both FAIL with
remediation (disable the `python3` App execution alias, or install real
Python ahead of WindowsApps on `PATH`); `not-found` folds into the
floor's absent-interpreter FAIL. A bare `command -v python3` success is
explicitly called out as insufficient — it matches the stub too.
- **README** requirements section documents the vector and *why* it
fails open.
- **Version bump → `0.9.2`** in `plugin.json` + matching `CHANGELOG.md`
entry. (Re-versioned three times as `main` moved under the PR — 0.6.5 →
0.6.6 after PR #1118, 0.9.0 → 0.9.1 after the kill-switch release landed
as 0.9.0, then 0.9.1 → 0.9.2 after #1305 took 0.9.1; each time by
merging `origin/main` in and re-slotting the CHANGELOG entry on top.
Merge, not rebase, per branch policy. The merged tree confirms the
vector still holds post-0.9.0: both guard surfaces — the plugin-level
engine gate in `hooks/hooks.json` and the skill-scoped belt — still
launch via the literal command `python3`.)

## Verification

- **New unit tests** `test_python3_alias_probe.py` (10 cases, run via
`python3_alias_probe.test.sh`) — all pass. They fabricate a zero-length
`.../WindowsApps/python3.exe` in a tempdir so detection is exercised
**cross-platform** without a real Windows box: stub detected;
case-insensitive `WindowsApps` match; real interpreter under a versioned
WindowsApps subdir → `ok` (no false positive); zero-length *outside*
WindowsApps → `ok`; `not-found`; the `indeterminate` stat-failure path
(fail-closed); and an assertion that the probe **never** invokes
`subprocess` on the candidate.
- Full plugin suites green post-merge: setup probe 10/10,
`kill_switch_probe` unaffected, clean engine `test_hygiene.py` 112
passed (4 skipped).
- `markdownlint-cli2`, `shellcheck`, and `typos` clean on all changed
files; new scripts tracked `100755` (exec-bit lane parity with the
existing `kill_switch_probe.*`).
- **Empirical signal confirmation (this Windows 11 box):** inspected
`%LOCALAPPDATA%\Microsoft\WindowsApps` — all 35 App-Execution-Alias
`.exe` stubs are `Length 0` with the ReparsePoint attribute set,
confirming the zero-length detection signal against ground truth (not
asserted from memory). Post-0.9.0-merge re-verification on the same box:
the probe run with `--path` pointed at a live AppExecLink stub returns
`store-alias-stub` (with `reparse_point: true` corroboration), the real
interpreter resolution returns `ok`, and an unreadable WindowsApps path
returns `indeterminate` (fail-closed).
- **Hook-failure-semantics claim verified live against current docs**
(fetched this session):
[code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks) —
"**Exit 2** … `PreToolUse` blocks the tool call"; "**Any other exit
code** is a non-blocking error … Execution continues"; PreToolUse can
alternatively block via exit-0 JSON `permissionDecision: deny`. A guard
that never launches emits neither exit 2 nor a `deny`, so the
non-blocking path is taken — the doc-grounded basis for the fail-open,
stated as that logical consequence rather than as a documented
launch-failure rule.

## Related

- Source: handoff-inbox item
`20260723-021058-disk-hygiene-0-6-4-consumer-audit`, finding **F3**.
- **Root cause note (out of scope here):** the terminal root cause is
the guard hook using the bare name `python3` (`skills/clean/SKILL.md`),
which changing the hook's launch command would address — that is
guard-registration territory (operator-gated, #1107), so this PR is the
assigned *mitigation* (detect + document + fail closed in `setup
check`), not a change to the guard's registration.
- Deferred low-risk items surfaced by independent review, none blocking:
`python3_alias_probe.test.sh`'s `command -v python3` fallback could
itself resolve to a Store stub on a stub-only machine (kept at parity
with the sibling `kill_switch_probe.test.sh` wrapper rather than
diverging); a symlink whose own path is outside WindowsApps but whose
target is inside would evade the path-component check (AppExecLink
aliases are reparse points, not symlinks, so not the real-world shape).

Closes #1110

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

disk-hygiene: doc corrections — Windows mislabeled 'full', false CLAUDE_PLUGIN_DATA premise repeated, PowerShell-lane gaps

1 participant