Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/disk-hygiene/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "disk-hygiene",
"version": "0.13.0",
"version": "0.15.0",
"description": "Context-aware disk hygiene for arbitrary directory trees: inventories orphaned and temporary artifacts, classifies evidence into review tiers, and offers exact-path cleanup only after a fresh safety preview and explicit per-tier approval. The target is read-only by default; OS-managed paths, links and mount points, VCS-tracked content, changed entries, and live-handle uncertainty fail closed.",
"author": {
"name": "Melodic Software",
Expand Down
40 changes: 40 additions & 0 deletions plugins/disk-hygiene/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,46 @@
All notable changes to the `disk-hygiene` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.15.0]

> Version note: `0.14.0` is claimed by PR #1870, open against this manifest. This entry takes the
> next number so the two do not collide.

### Fixed

- **A confirmation question is unanswerable when its acceptance bar names something the question
never showed.** The gate applied one bar — "an affirmative answer naming exactly the tier and path
list just shown" — to every question the skill asks, including the no-target prompt and §1's
large-scan confirmation. Neither has presented a tier or a path list, so no reply a human could
give satisfied the stated bar, and the two questions the gate exists to protect were the only ones
it could actually be cleared for. The question surface rule and the answer floor (the user's own
answer, this session, never inferred, stop on rejection) stay common to all four questions; what an
answer must *name* is now stated per question — a directory for target selection, the target plus a
deliberate unbounded walk for scan scope, the exact tier and path list for removal and the manual
handoff. §1's and §6's cross-references now name their row instead of asserting the deletion bar
applies unchanged, and the gate states the obligation that generated the defect: ask each question
so it shows what its row requires the answer to name.

- **The confirmation gate fell back to an inline question only when `AskUserQuestion` was
*absent*.** Permission mode `dontAsk` "auto-denies tools unless pre-approved … `AskUserQuestion` …
denied even if you've allowed them"
([permissions](https://code.claude.com/docs/en/permissions), fetched 2026-08-08), which leaves the
tool visible in the pool while every call fails; only a bare-name deny rule "removes the tool from
Claude's context entirely". Absence and denial are therefore distinct states, and keying the
fallback on absence let a `dontAsk` session pick a tool it cannot use and leave the destructive
confirmation gate unsatisfied rather than asking inline. The fallback now triggers on absent,
denied, **or otherwise unusable** — including a denial discovered only by calling it — so a state
neither named case anticipates still routes to the inline question.

- **The `python3` alias probe could not be reached on a machine whose only alternate interpreter
cannot run it.** `setup` step 1(b) classifies the `python3` resolution with a bundled inspect-only
probe launched through some other interpreter, and routed to the PowerShell equivalent only when no
such interpreter existed at all. A real-but-incompatible launcher — Python 3.6, which the same
section already names as an interpreter that rejects `from __future__ import annotations`, or a
legacy `python` 2.x — is not absent, so the check had no path to a verdict and could classify
neither the Store stub nor its own remediation. The PowerShell fallback now also covers a chosen
interpreter that emits no verdict.

## [0.13.0]

> Version note: `0.11.0` is claimed by #1804 (PR #1818) and `0.12.0` by #1805 (PR #1819), both open
Expand Down
51 changes: 33 additions & 18 deletions plugins/disk-hygiene/skills/clean/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,32 @@ unless bounded with `--max-depth` or confirmed with `--confirmed-large-scan`.

## Confirmation gate

Every question this skill asks passes this gate — including the no-target prompt above, the
large-scan confirmation in §1, the removal approval in §5, and the unsupported-platform handoff
in §6.
Every question this skill asks passes this gate — the no-target prompt above, the large-scan
confirmation in §1, the removal approval in §5, and the unsupported-platform handoff in §6. One
surface rule and one floor cover all four. What a valid answer must *name* is per question, because
a target prompt has no tier or path list to name and cannot be held to a bar built for one.

**Question surface.** Prefer `AskUserQuestion`: its answer is the user's own and cannot be
fabricated. It is not always in the pool — permission mode `dontAsk` denies it unconditionally, and
a bare-name `permissions.deny` rule or a `disallowed-tools` entry removes it — so when it is absent,
ask the same question inline as a numbered choice and wait for the reply. The surface varies; the bar
below does not.

**The bar.** Take the user's own affirmative answer, given in this interactive session, naming
exactly the tier and path list just shown. A prior general request, `--execute`, "clean everything",
approval of another tier, or silence is not confirmation — never supply or infer the answer
yourself. On rejection, stop.
fabricated. It is not always usable, in two distinct ways — a bare-name `permissions.deny` rule or a
`disallowed-tools` entry removes it from context entirely, while permission mode `dontAsk` denies it
even when an allow rule names it, leaving it visible and every call failing. Fall back to the same
question asked inline as a numbered choice whenever the tool is absent, denied, **or otherwise
unusable** — including a denial discovered only by calling it; a denied call is an unanswered
question, never an answer. Then wait for the reply.

**The floor — every question.** Take the user's own answer, given in this interactive session. Never
supply, infer, or fabricate it: a prior general request, `--execute`, "clean everything", approval of
another tier, or silence is not an answer. On rejection, stop.

**What the answer must name — per question.** Where a row requires the answer to name something the
skill itself produced — the resolved target, the tier, the path list — show it in the question; a bar
naming what the question never presented cannot be met.

| Question | Accept only an answer naming |
|---|---|
| Target selection (no target given) | one directory, which must then clear every rejection in "Arguments and boundaries" |
| Scan scope (`--confirmed-large-scan`, §1) | that target and a deliberate unbounded full walk of it |
| Removal approval (§5) and manual handoff (§6) | exactly the one tier and the exact path list just shown |

## 1. Create a read-only snapshot

Expand Down Expand Up @@ -116,8 +128,9 @@ this gate) and carries neither `--max-depth` nor `--confirmed-large-scan` return
unbounded traversal, so a forgotten bound never becomes an accidental whole-volume scan. `--max-depth`
is the preferred bounded response.
Reserve `--confirmed-large-scan` for a deliberate full walk the human has confirmed — pass the
[confirmation gate](#confirmation-gate) first, exactly as the apply lane requires before an
expensive step; a general "clean my home directory" is not that confirmation. Every
[confirmation gate](#confirmation-gate)'s scan-scope row first, the same standing before an
expensive step that the apply lane demands before a destructive one; a general "clean my home
directory" is not that confirmation. Every
directory whose descendants were not walked — cut off by `--max-depth`, a protected root, or a VCS
boundary — is recorded in `truncated_paths`; report them as coverage gaps, never as clean, and
never plan them for removal (the preview blocks them as `truncated-not-inventoried` and skips the
Expand Down Expand Up @@ -249,10 +262,12 @@ token.
Preview reports `execution-platform-unsupported` as a per-candidate blocker on these platforms, so
the engine never deletes there. The default outcome is the report. The manual lane is gated by
`--execute` exactly as the engine lane is — without it, no deletion lane may be offered on any
platform. If — and only if — `--execute` was requested and the human reviews the report and
approves an exact path list in this interactive session (the same [confirmation gate](#confirmation-gate)
exact-tier-and-list bar as the engine lane; a general "clean it up" is still not approval), removal is a manual
handoff, not an engine plan:
platform. If — and only if — `--execute` was requested and the human reviews the report and approves
an exact path list drawn from one tier in this interactive session (the §3 report spans every tier, so
narrow it to a single tier and show that tier's paths before asking — the
[confirmation gate](#confirmation-gate)'s removal row is the same exact-tier-and-list bar the engine
lane clears; a general "clean it up" is still not approval), removal is a manual handoff, not an
engine plan:

1. Write the approved exact paths to `<run-dir>/handoff-paths.json` as
`{"version": 1, "paths": ["relative/exact.tmp"]}` (snapshot-relative, exact, non-overlapping,
Expand Down
8 changes: 6 additions & 2 deletions plugins/disk-hygiene/skills/setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,12 @@ fails closed like every other guard-relevant unknown in this plugin.
`python3` (`py -3`, `python`, or an absolute interpreter path — any interpreter already
proven real):
`"<python>" "${CLAUDE_PLUGIN_ROOT}/skills/setup/scripts/python3_alias_probe.py"`; if no
such interpreter exists, apply the probe's own portable signal directly in PowerShell — a
zero-length file under a `WindowsApps` path component is the stub
such interpreter exists **or the one you chose emits no JSON verdict**, apply the probe's own
portable signal directly in PowerShell. Proven real is not the same as able to run the probe:
a pre-3.7 interpreter rejects its `from __future__ import annotations` and a legacy `python`
2.x fails earlier still, each before anything is classified — so a machine whose only
alternate launcher is Python 3.6 reaches the verdict through PowerShell, not by having no
launcher at all. The signal: a zero-length file under a `WindowsApps` path component is the stub
(`(Get-Item -Force (Get-Command python3).Source)` → `Length` 0 plus a `ReparsePoint`
attribute); (c) only after the verdict is `ok` may the version probe execute `python3` —
and distinguish its two failure modes for the remediation wording: an interpreter that
Expand Down