Purpose
Thin check-centric setup per the uniform setup contract (docs/PLUGIN-PHILOSOPHY.md
"Setup is explicit and repeatable" in the marketplace repository): check inspects and
reports, apply resolves. This plugin owns no consumer-project configuration, targets
and modes arrive as /disk-hygiene:clean arguments, and the only tunable is the native
userConfig toggle, so apply is pure guidance and writes nothing.
Action routing: no argument or check runs the check; apply runs the check first, then
points at each remediation. Both are non-interactive, never prompt when the action is given.
check (read-only)
The clean skill and its bundled scripts (${CLAUDE_PLUGIN_ROOT}/skills/clean/) are the
single source of truth for what the plugin requires per platform.
Read it first. Probe what it actually does, don't recite this file. Then run each probe via
Bash and report a PASS/FAIL/INFO table with one remediation line per FAIL. Do not modify anything.
The toggle is an audit-only kill switch, not a short-circuit: the guard registers
unconditionally and still scans with the toggle off, so most prerequisite absences keep their
FAIL semantics regardless of the toggle. Only the execution-tier tools the audit lane never
invokes (step 4's lsof-class probes) downgrade from FAIL to INFO when the toggle is
disabled, report those informationally and note that re-enabling restores the FAIL semantics.
A missing git stays FAIL either way: the audit lane itself calls it, and its absence degrades
the VCS-tracked-content evidence the plugin's fail-closed posture depends on.
Every step-1 and step-2 failure likewise stays FAIL with the toggle disabled.
Audit-only mode is enforced by the guard, every guard surface
depends on a Python 3 interpreter resolving (every surface through
hooks/run-python-hook.sh, which tries python3, then python, then py -3), and a
guard that never runs can neither read nor enforce the configured false, so the fail-open
is most dangerous in exactly this
configuration. That covers an exhausted interpreter ladder, every rung absent, a stub, or
indeterminate, a rung whose version probe then fails to launch at all (a corrupt or
zero-length binary outside WindowsApps, a broken shim, a permission error), and an
interpreter that starts but reports a version below the parsed floor. It does not cover a
stubbed python3 alongside a working python or py -3: the launcher skips the stub, every
guard launches, and that is a WARN (see step 2), not a failure to downgrade.
Launching the version probe proves only that something executes, not that it can run
the guard's own source: Python 3.6, for example, rejects the guard's
from __future__ import annotations and exits without a deny, which PreToolUse treats as
non-blocking, the same silent fail-open through a different door. Unproven guard execution
fails closed like every other guard-relevant unknown in this plugin.
Shell-form launcher registration. All three registrations (both wired hooks in
hooks/hooks.json and the skill-scoped belt in skills/clean/SKILL.md frontmatter) must name
hooks/run-python-hook.sh directly in command, with "shell": "bash" and no args, so
Claude Code routes them through its own Git Bash rather than a PATH lookup. FAIL if any
registration carries an args key or sets command to a bare interpreter name such as bash
or python3: that is exec form, which on Windows resolves bash to the WSL relay
System32\bash.exe and python3 to the zero-length WindowsApps App Execution Alias stub, and
fails to launch, and a failed hook launch is non-blocking, so the guard silently enforces
nothing. Do not report this as a
PATH-ordering problem: shell form is resolved by
Claude Code, so reordering PATH neither causes nor fixes it. Also FAIL if the launcher is
missing or not executable. Report a missing Git Bash on Windows as an environment prerequisite
(shell form falls back to PowerShell there, which cannot run a .sh), not as a PATH fix.
Python floor on PATH. The interpreter used by scanning, validation, the
guard, and cleanup. (The guard registers on two surfaces: a plugin-level engine gate
that acts only on engine-referencing commands, and the skill-frontmatter belt that
Claude Code keeps armed for the rest of the session after /disk-hygiene:clean is
invoked. Both register unconditionally and resolve the kill switch by
reading disk_hygiene_enabled from managed settings first, then the user settings.json.) The required version has one origin: the MIN_PYTHON
constant in ${CLAUDE_PLUGIN_ROOT}/skills/clean/scripts/hygiene.py, parse it from
there (grep -m1 '^MIN_PYTHON' …) and probe the interpreter against that value; do not
recite a version number from this file or the README. FAIL if absent or older, naming
the parsed floor in the remediation; the plugin never downloads a runtime. Report the
absolute interpreter path (guarded engine calls must use the same absolute interpreter
the guard reports. Bash aliases and functions cannot substitute).
On Windows, confirm the name the guard resolves is real BEFORE anything executes it,
including this floor check's own version probe. python3 is the first rung of the ladder
hooks/run-python-hook.sh walks for every guard surface; on stock Windows it resolves to a
zero-length WindowsApps\python3.exe App Execution Alias that opens the Microsoft Store
(or hangs) instead of running an interpreter, and executing that name from setup pops the
Store instead of probing. The stub does not stop any guard by itself: the launcher skips it
and falls through to python, then py -3. The ladder, not the first
rung, is the verdict. A host with real Python installed without "Add to PATH" but with the
py launcher has a stubbed python3 and a perfectly working guard; failing it would report a
healthy install as broken and send the operator to reinstall Python. So the alias probe is
diagnostic input. It says what the first rung is, and keeps setup from executing it,
while the verdict comes from resolving the ladder and checking the selected interpreter
against the parsed floor.
Order of operations: (a) locate the resolution without executing it (Get-Command python3
/ command -v python3, locating is inspection; running is not); (b) classify it with the
bundled inspect-only probe, launched via an interpreter that is NOT the bare name
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 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) resolve the ladder in the launcher's own order, skipping any rung the probe
classified as a stub: python3 only when its verdict is ok, then python, then py -3.
The version probe may execute a rung only once that rung is not a stub, that is the whole
point of (b), and it is why the bare name python3 is never executed on a
store-alias-stub verdict. Report the absolute path of the first rung that runs and meets
the floor.
FAIL when the ladder is exhausted or below the floor. No rung resolves, or the one that
does reports a version under the parsed MIN_PYTHON. That is the real fail-open: the guard
cannot run, and it emits neither exit 2 nor a deny. Distinguish the two for the remediation
wording, an interpreter that starts and reports a version below the floor is a floor miss
(name the parsed floor), one that fails to launch at all is an absent-interpreter failure
(the plugin never downloads a runtime). Both keep the FAIL under a disabled toggle: a
below-floor interpreter is not proven able to execute the guard's source, so audit-only mode
is unenforceable there too. indeterminate on every rung is the same case, identity the
probe could not read anywhere on the ladder is uncertainty about whether the guard can launch
at all, and fails closed like every other guard-relevant unknown in this plugin.
PASS with a WARN when the ladder resolves a supported interpreter but python3 is the
stub. Every guard launches, so nothing is failing open. State the residual plainly: the
operator's own bare python3 still opens the Microsoft Store, and guarded engine calls must
use the absolute interpreter path reported above rather than that name. Offer the same
remediation as an optional tidy-up, not as a fix for a broken install, disable the python3
App execution alias (Settings > Apps > Advanced app settings > App execution aliases), or put
real Python ahead of WindowsApps on PATH. A bare command -v python3 success is not
evidence on its own, it matches the stub too.
Git. command -v git. Conditional per the README: optional for ordinary trees,
required when a target contains or sits inside a Git worktree. Report presence as INFO
with that conditionality stated; absence is only a FAIL for worktree-containing targets.
Platform posture. Detect the current OS family and report its documented lane per
the README, keeping the audit and execution lanes visibly separate: Windows (full
audit: lstat reparse + Win32, never UAC; engine execution unsupported.
preview reports execution-platform-unsupported as a per-candidate blocker, removal is
a manual, per-path Recycle-Bin handoff only under --execute and after explicit
approval), Linux (full audit; execution when
/proc/self/mountinfo is readable, lsof needed only for that optional execution
lane, absent lsof is INFO with the reduced-capability note), macOS (audit/report
only by design; manual Trash handoff only under --execute. INFO, not a defect).
Execution kill switch. Resolve the effective disk_hygiene_enabled value
deterministically; never present an assumed value as the configured one. Run the bundled
probe with the step-2 interpreter:
"<python>" "${CLAUDE_PLUGIN_ROOT}/skills/setup/scripts/kill_switch_probe.py"
and report its effective value together with its source (configured vs default).
When the probe says degraded: true, report that the configured value could not be read
and that default true is being assumed, an assumption, never the configured value. The
body token ${user_config.disk_hygiene_enabled} is at most a cross-check: if it expanded
to a boolean that contradicts the probe, report the discrepancy instead of silently
preferring either channel (the probe sees user settings only; managed settings or a
--settings flag can carry a value the probe cannot see).
Plugin registration. INFO: confirm the plugin is enabled for this project
(/plugin → Installed) rather than parsing settings files.
apply (idempotent)
Run check, then for each FAIL point at the resolution. Every prerequisite is a system
tool or an OS capability, so apply installs nothing and writes nothing, it only points:
- missing/old Python: the platform's own install channel for the floor
check parsed from
the engine's MIN_PYTHON; never a plugin download.
- missing git (worktree targets): platform install instructions.
- toggle off: reconfigure through Claude Code's native flow, per the marketplace's
plugin-reconfiguration convention, which owns the verified-version record
(https://github.com/melodic-software/claude-code-plugins/blob/main/docs/conventions/plugin-reconfiguration/README.md):
interactive
/plugin configure disk-hygiene@<marketplace> any time, or headless
claude plugin install disk-hygiene@<marketplace> -s <scope> --config disk_hygiene_enabled=true
(repeatable per key) — against an already-installed plugin it prints already installed and
still writes the value. Do not uninstall to reconfigure: uninstalling drops this plugin's
entire stored pluginConfigs entry, resetting every option in the README's Options reference
to its manifest default. -s defaults to user; pass the scope claude plugin list reports
for this plugin, and run from that project's directory for a project/local scope, or the
write lands at a scope that does not load. This skill never writes user settings or
pluginConfigs. Afterwards rerun check in a fresh session — the rendered
${user_config.*} is injected at skill load and each hook receives its
CLAUDE_PLUGIN_OPTION_* from an environment fixed at session start, so a same-session check
still reports the OLD value; report the observed effective value, never an unobserved change.
Re-running apply after everything passes changes nothing and reports "already configured".
What this skill does NOT do
- Run an audit or cleanup, that is
/disk-hygiene:clean.
- Write the plugin cache, Claude Code user settings, or
pluginConfigs.
- Install any tool or runtime, during either
check or apply, guidance only.
1---2name: setup-333description: Verify the disk-hygiene plugin's runtime prerequisites and platform posture for this machine. Use when: 'set up disk-hygiene', 'configure disk-hygiene', 'is disk-hygiene working', a clean run reported a missing prerequisite, or before a first audit on a new machine. Actions: check (read-only verification, default) | apply (resolve what check found). Re-runnable and safe.4---56## Purpose78Thin check-centric setup per the uniform setup contract (`docs/PLUGIN-PHILOSOPHY.md`9"Setup is explicit and repeatable" in the marketplace repository): `check` inspects and10reports, `apply` resolves. This plugin owns no consumer-project configuration, targets11and modes arrive as `/disk-hygiene:clean` arguments, and the only tunable is the native12`userConfig` toggle, so `apply` is pure guidance and writes nothing.1314Action routing: no argument or `check` runs the check; `apply` runs the check first, then15points at each remediation. Both are non-interactive, never prompt when the action is given.1617## `check` (read-only)1819The clean skill and its bundled scripts (`${CLAUDE_PLUGIN_ROOT}/skills/clean/`) are the20single source of truth for what the plugin requires per platform.2122**Read it first.** Probe what it actually does, don't recite this file. Then run each probe via23Bash and report a PASS/FAIL/INFO table with one remediation line per FAIL. Do not modify anything.2425The toggle is an audit-only kill switch, not a short-circuit: the guard registers26unconditionally and still scans with the toggle off, so most prerequisite absences keep their27FAIL semantics regardless of the toggle. Only the execution-tier tools the audit lane never28invokes (step 4's `lsof`-class probes) downgrade from FAIL to INFO when the toggle is29disabled, report those informationally and note that re-enabling restores the FAIL semantics.30A missing `git` stays FAIL either way: the audit lane itself calls it, and its absence degrades31the VCS-tracked-content evidence the plugin's fail-closed posture depends on.3233Every step-1 and step-2 failure likewise stays FAIL with the toggle disabled.34Audit-only mode is *enforced by* the guard, every guard surface35depends on a Python 3 interpreter resolving (every surface through36`hooks/run-python-hook.sh`, which tries `python3`, then `python`, then `py -3`), and a37guard that never runs can neither read nor enforce the configured `false`, so the fail-open38is most dangerous in exactly this39configuration. That covers an **exhausted** interpreter ladder, every rung absent, a stub, or40`indeterminate`, a rung whose version probe then fails to launch at all (a corrupt or41zero-length binary outside `WindowsApps`, a broken shim, a permission error), *and* an42interpreter that starts but reports a version below the parsed floor. It does **not** cover a43stubbed `python3` alongside a working `python` or `py -3`: the launcher skips the stub, every44guard launches, and that is a WARN (see step 2), not a failure to downgrade.45Launching the version probe proves only that something executes, not that it can run46the guard's own source: Python 3.6, for example, rejects the guard's47`from __future__ import annotations` and exits without a deny, which PreToolUse treats as48non-blocking, the same silent fail-open through a different door. Unproven guard execution49fails closed like every other guard-relevant unknown in this plugin.50511. **Shell-form launcher registration**. All three registrations (both wired hooks in52 `hooks/hooks.json` and the skill-scoped belt in `skills/clean/SKILL.md` frontmatter) must name53 `hooks/run-python-hook.sh` directly in `command`, with `"shell": "bash"` and **no `args`**, so54 Claude Code routes them through its own Git Bash rather than a `PATH` lookup. FAIL if any55 registration carries an `args` key or sets `command` to a bare interpreter name such as `bash`56 or `python3`: that is exec form, which on Windows resolves `bash` to the WSL relay57 `System32\bash.exe` and `python3` to the zero-length `WindowsApps` App Execution Alias stub, and58 fails to launch, and a failed hook launch is non-blocking, so the guard silently enforces59 nothing. Do **not** report this as a60 `PATH`-ordering problem: shell form is resolved by61 Claude Code, so reordering `PATH` neither causes nor fixes it. Also FAIL if the launcher is62 missing or not executable. Report a missing Git Bash on Windows as an environment prerequisite63 (shell form falls back to PowerShell there, which cannot run a `.sh`), not as a `PATH` fix.642. **Python floor on `PATH`**. The interpreter used by scanning, validation, the65 guard, and cleanup. (The guard registers on two surfaces: a plugin-level engine gate66 that acts only on engine-referencing commands, and the skill-frontmatter belt that67 Claude Code keeps armed for the rest of the session after `/disk-hygiene:clean` is68 invoked. Both register unconditionally and resolve the kill switch by69 reading `disk_hygiene_enabled` from managed settings first, then the user `settings.json`.) The required version has one origin: the `MIN_PYTHON`70 constant in `${CLAUDE_PLUGIN_ROOT}/skills/clean/scripts/hygiene.py`, parse it from71 there (`grep -m1 '^MIN_PYTHON' …`) and probe the interpreter against that value; do not72 recite a version number from this file or the README. FAIL if absent or older, naming73 the parsed floor in the remediation; the plugin never downloads a runtime. Report the74 absolute interpreter path (guarded engine calls must use the same absolute interpreter75 the guard reports. Bash aliases and functions cannot substitute).7677 On Windows, confirm the name the guard resolves is real BEFORE anything executes it,78 including this floor check's own version probe. `python3` is the first rung of the ladder79 `hooks/run-python-hook.sh` walks for every guard surface; on stock Windows it resolves to a80 zero-length `WindowsApps\python3.exe` App Execution Alias that opens the Microsoft Store81 (or hangs) instead of running an interpreter, and executing that name from setup pops the82 Store instead of probing. The stub does not stop any guard by itself: the launcher skips it83 and falls through to `python`, then `py -3`. **The ladder, not the first84 rung, is the verdict.** A host with real Python installed without "Add to PATH" but with the85 `py` launcher has a stubbed `python3` and a perfectly working guard; failing it would report a86 healthy install as broken and send the operator to reinstall Python. So the alias probe is87 **diagnostic input**. It says what the first rung is, and keeps setup from executing it,88 while the verdict comes from resolving the ladder and checking the selected interpreter89 against the parsed floor.90 Order of operations: (a) locate the resolution without executing it (`Get-Command python3`91 / `command -v python3`, locating is inspection; running is not); (b) classify it with the92 bundled inspect-only probe, launched via an interpreter that is NOT the bare name93 `python3` (`py -3`, `python`, or an absolute interpreter path, any interpreter already94 proven real):95 `"<python>" "${CLAUDE_PLUGIN_ROOT}/skills/setup/scripts/python3_alias_probe.py"`; if no96 such interpreter exists **or the one you chose emits no JSON verdict**, apply the probe's own97 portable signal directly in PowerShell. Proven real is not the same as able to run the probe:98 a pre-3.7 interpreter rejects its `from __future__ import annotations` and a legacy `python`99 2.x fails earlier still, each before anything is classified, so a machine whose only100 alternate launcher is Python 3.6 reaches the verdict through PowerShell, not by having no101 launcher at all. The signal: a zero-length file under a `WindowsApps` path component is the stub102 (`(Get-Item -Force (Get-Command python3).Source)` → `Length` 0 plus a `ReparsePoint`103 attribute); (c) resolve the ladder in the launcher's own order, skipping any rung the probe104 classified as a stub: `python3` only when its verdict is `ok`, then `python`, then `py -3`.105 The version probe may execute a rung only once that rung is not a stub, that is the whole106 point of (b), and it is why the bare name `python3` is never executed on a107 `store-alias-stub` verdict. Report the absolute path of the first rung that runs and meets108 the floor.109110 **FAIL when the ladder is exhausted or below the floor**. No rung resolves, or the one that111 does reports a version under the parsed `MIN_PYTHON`. That is the real fail-open: the guard112 cannot run, and it emits neither exit 2 nor a `deny`. Distinguish the two for the remediation113 wording, an interpreter that starts and reports a version below the floor is a floor miss114 (name the parsed floor), one that fails to launch at all is an absent-interpreter failure115 (the plugin never downloads a runtime). Both keep the FAIL under a disabled toggle: a116 below-floor interpreter is not proven able to execute the guard's source, so audit-only mode117 is unenforceable there too. `indeterminate` on **every** rung is the same case, identity the118 probe could not read anywhere on the ladder is uncertainty about whether the guard can launch119 at all, and fails closed like every other guard-relevant unknown in this plugin.120121 **PASS with a WARN when the ladder resolves a supported interpreter but `python3` is the122 stub.** Every guard launches, so nothing is failing open. State the residual plainly: the123 operator's own bare `python3` still opens the Microsoft Store, and guarded engine calls must124 use the absolute interpreter path reported above rather than that name. Offer the same125 remediation as an optional tidy-up, not as a fix for a broken install, disable the `python3`126 App execution alias (Settings > Apps > Advanced app settings > App execution aliases), or put127 real Python ahead of WindowsApps on `PATH`. A bare `command -v python3` success is not128 evidence on its own, it matches the stub too.1293. **Git**. `command -v git`. Conditional per the README: optional for ordinary trees,130 required when a target contains or sits inside a Git worktree. Report presence as INFO131 with that conditionality stated; absence is only a FAIL for worktree-containing targets.1324. **Platform posture**. Detect the current OS family and report its documented lane per133 the README, keeping the audit and execution lanes visibly separate: Windows (full134 **audit**: `lstat` reparse + Win32, never UAC; engine **execution unsupported**.135 `preview` reports `execution-platform-unsupported` as a per-candidate blocker, removal is136 a manual, per-path Recycle-Bin handoff only under `--execute` and after explicit137 approval), Linux (full audit; execution when138 `/proc/self/mountinfo` is readable, `lsof` needed only for that optional execution139 lane, absent `lsof` is INFO with the reduced-capability note), macOS (audit/report140 only by design; manual Trash handoff only under `--execute`. INFO, not a defect).1415. **Execution kill switch**. Resolve the effective `disk_hygiene_enabled` value142 deterministically; never present an assumed value as the configured one. Run the bundled143 probe with the step-2 interpreter:144 `"<python>" "${CLAUDE_PLUGIN_ROOT}/skills/setup/scripts/kill_switch_probe.py"`145 and report its `effective` value together with its `source` (`configured` vs `default`).146 When the probe says `degraded: true`, report that the configured value could not be read147 and that default `true` is being assumed, an assumption, never the configured value. The148 body token `${user_config.disk_hygiene_enabled}` is at most a cross-check: if it expanded149 to a boolean that contradicts the probe, report the discrepancy instead of silently150 preferring either channel (the probe sees user settings only; managed settings or a151 `--settings` flag can carry a value the probe cannot see).1526. **Plugin registration**. INFO: confirm the plugin is enabled for this project153 (`/plugin` → Installed) rather than parsing settings files.154155## `apply` (idempotent)156157Run `check`, then for each FAIL point at the resolution. Every prerequisite is a system158tool or an OS capability, so `apply` installs nothing and writes nothing, it only points:159160- missing/old Python: the platform's own install channel for the floor `check` parsed from161 the engine's `MIN_PYTHON`; never a plugin download.162- missing git (worktree targets): platform install instructions.163- toggle off: reconfigure through Claude Code's native flow, per the marketplace's164 plugin-reconfiguration convention, which owns the verified-version record165 (<https://github.com/melodic-software/claude-code-plugins/blob/main/docs/conventions/plugin-reconfiguration/README.md>):166 interactive `/plugin configure disk-hygiene@<marketplace>` any time, or headless167 `claude plugin install disk-hygiene@<marketplace> -s <scope> --config disk_hygiene_enabled=true`168 (repeatable per key) — against an already-installed plugin it prints `already installed` **and169 still writes the value**. Do **not** uninstall to reconfigure: uninstalling drops this plugin's170 entire stored `pluginConfigs` entry, resetting every option in the README's Options reference171 to its manifest default. `-s` defaults to `user`; pass the scope `claude plugin list` reports172 for this plugin, and run from that project's directory for a `project`/`local` scope, or the173 write lands at a scope that does not load. This skill never writes user settings or174 `pluginConfigs`. Afterwards rerun `check` in a **fresh session** — the rendered175 `${user_config.*}` is injected at skill load and each hook receives its176 `CLAUDE_PLUGIN_OPTION_*` from an environment fixed at session start, so a same-session `check`177 still reports the OLD value; report the observed effective value, never an unobserved change.178179Re-running `apply` after everything passes changes nothing and reports "already configured".180181## What this skill does NOT do182183- Run an audit or cleanup, that is `/disk-hygiene:clean`.184- Write the plugin cache, Claude Code user settings, or `pluginConfigs`.185- Install any tool or runtime, during either `check` or `apply`, guidance only.