|
| 1 | +--- |
| 2 | +description: "Read-only, audit-only scan of one directory tree for orphaned, temporary, stale-lock, failed-write, partial-download, and empty leftovers, reported as an evidence snapshot. Removes nothing; any removal is a separate /disk-hygiene:clean run that a person invokes. Use when: 'delegate a disk-hygiene audit', 'orchestrator scan of a directory for leftovers', 'subagent scan of this directory for leftovers', 'disk-hygiene audit report'. Skip when: the ask is to clean up or reclaim space (that is /disk-hygiene:clean), one repository's caches or build output (repo-hygiene), or an OS-managed root." |
| 3 | +argument-hint: "[--max-depth <N>] [--sizes-only] [--policy <file>] <target-directory>" |
| 4 | +user-invocable: true |
| 5 | +disable-model-invocation: false |
| 6 | +metadata: |
| 7 | + workflow-stage: anytime |
| 8 | + summary: Scan a directory tree for stale leftovers and report the evidence, read-only |
| 9 | +--- |
| 10 | + |
| 11 | +**Arguments.** `[--max-depth <N>] [--sizes-only] [--policy <file>] <target-directory>`. Full form: `[--max-depth <N>] [--sizes-only] [--quiet] [--policy <file>] [--root-children [--root-child <name>]...] <target-directory>` |
| 12 | + |
| 13 | +# Disk hygiene audit |
| 14 | + |
| 15 | +Scan one directory tree and report what the snapshot shows. This skill runs the engine's `scan` |
| 16 | +subcommand and nothing else, and it changes nothing in the target. A filename pattern is a |
| 17 | +discovery hint, never proof that an entry is junk. |
| 18 | + |
| 19 | +This skill carries no hooks of its own. Safety rests on the plugin-level engine-gate in |
| 20 | +`hooks/hooks.json`, which checks every Bash or PowerShell call that names `hygiene.py`. Run no |
| 21 | +engine subcommand other than `scan`, no shell command that writes, moves, or deletes under the |
| 22 | +target, and no compound shell around an engine call. A subagent follows that contract itself, from |
| 23 | +the worker brief in step 2. |
| 24 | + |
| 25 | +## 1. Bootstrap |
| 26 | + |
| 27 | +Run the argument-free probe first, before any engine call: |
| 28 | + |
| 29 | +```text |
| 30 | +"<hook-python>" "${CLAUDE_PLUGIN_ROOT}/skills/setup/scripts/kill_switch_probe.py" |
| 31 | +``` |
| 32 | + |
| 33 | +Take `hook_python` and `data_root` from its one-line JSON. Every engine call needs the absolute |
| 34 | +`<hook-python>` and `--data-root`; a bare `python3` is rejected. When `hook_python` is not yet |
| 35 | +known, submit the probe once with bare `python`. The probe reports `hook_python` as the interpreter |
| 36 | +it ran under, so that value is the guard's only if the guard's own interpreter ran it. The scan is |
| 37 | +admitted only under the guard's interpreter: if it is denied for that reason, the denial names the |
| 38 | +interpreter and ran nothing, so rerun the scan with the interpreter it names. |
| 39 | + |
| 40 | +- `data_root` is `null`: the install layout proved no data root and the guard denies every engine |
| 41 | + call. Report the audit as not run, submit no engine call, and stop. |
| 42 | +- The probe call is denied or left waiting for a person: report the audit as not run and stop. Do |
| 43 | + not scan without it. |
| 44 | +- The guard's interpreter is older than `MIN_PYTHON` in |
| 45 | + [`hygiene.py`](../clean/scripts/hygiene.py): stop with that declared prerequisite. An older |
| 46 | + `hook_python` from a bare-`python` probe is not that finding; the scan's admission settles it. |
| 47 | +- `effective` is `false` (audit-only): the scan still runs. State the configured value, and leave |
| 48 | + out the removal handoff in step 4. On `degraded: true`, say the configured value could not be read. |
| 49 | + |
| 50 | +## 2. Scan |
| 51 | + |
| 52 | +Pick a unique run directory under `<data_root>/runs/`; the snapshot lives there, never in the |
| 53 | +target. With no target, ask the person who invoked you once. Reject an OS-managed root, a missing |
| 54 | +directory, a symlink, or a Windows reparse point. |
| 55 | + |
| 56 | +```text |
| 57 | +"<hook-python>" "${CLAUDE_PLUGIN_ROOT}/skills/clean/scripts/hygiene.py" scan \ |
| 58 | + --target "<target>" --output "<run-dir>/snapshot.json" \ |
| 59 | + --data-root "<data_root>" [--project-dir "<project-dir>"] \ |
| 60 | + [--policy "<policy.json>"] [--max-depth <N>] [--sizes-only] [--quiet] \ |
| 61 | + [--root-children [--root-child <name>]...] |
| 62 | +``` |
| 63 | + |
| 64 | +`--project-dir` is optional; pass it, as a literal absolute path, when the consumer project has |
| 65 | +standing policy files. Pass literal values only: the guard rejects shell expansion, so never pass |
| 66 | +an environment-variable reference. What each flag does, including the large-target and volume-root rules, is in |
| 67 | +[scan-flags.md](../clean/reference/scan-flags.md). For a home directory or another large target, |
| 68 | +start with `--max-depth 1`, then scan the subtrees the evidence justifies. Never pass |
| 69 | +`--confirmed-large-scan` on your own: an unbounded walk needs a person's answer. |
| 70 | + |
| 71 | +For a subtree worker, the brief to paste into the spawn prompt is |
| 72 | +[fan-out-worker-brief.md](../clean/reference/fan-out-worker-brief.md). Before spawning, replace every |
| 73 | +`${...}` token and `<placeholder>` in it with the literal absolute value you hold from the probe |
| 74 | +(`hook_python`, `data_root`) or from step 2: a worker cannot expand `${...}` tokens, and its data |
| 75 | +root is the probe's `data_root`. A worker returns scan evidence only. |
| 76 | + |
| 77 | +## 3. Read and report the snapshot |
| 78 | + |
| 79 | +Report from the snapshot and the scan's stdout, and nothing the run did not observe. |
| 80 | + |
| 81 | +- Lead with `children_rollup`. `walked: true` rows carry exact totals; `walked: false` rows carry |
| 82 | + `null` aggregates and `unwalked_reasons`, so report them as coverage gaps, never as small or clean. |
| 83 | +- Rank on `reclaimable_local_bytes`, not `logical_bytes`. An entry whose `size_qualifiers` is |
| 84 | + non-empty stays out of any reclaimable total; state its bytes and reasons separately. |
| 85 | +- Report `truncated_paths` (a count under `--quiet`, the list in the snapshot) and every scan error |
| 86 | + as coverage gaps. A `large-target-confirmation-required` or `root-children-selection-required` |
| 87 | + status names the next step, not a failure. |
| 88 | +- Quote hint coverage as a rate: `hinted_entries` of `entries`, never "N findings". |
| 89 | +- List protected entries separately, and surface an `os_autoclean` recommendation as the engine |
| 90 | + states it. A scan does not assess live handles or elevation, so it yields no locked, |
| 91 | + needs-elevation, or unverified entries: say those were not assessed, never that there are none. |
| 92 | +- Give each hinted entry the evidence the snapshot holds (path, hint, `protected_reasons`, |
| 93 | + `size_qualifiers`). A hint has no owner check behind it, so label the list hints for a person to |
| 94 | + judge, not verdicts, and rank nothing for deletion. Empty directories stay visible. |
| 95 | +- A non-zero exit is a real failure: 2 for an invalid or blocked target, 3 when elevation is needed |
| 96 | + or filesystem state could not be verified. Report it and stop. |
| 97 | + |
| 98 | +## 4. Hand off |
| 99 | + |
| 100 | +Removal is `/disk-hygiene:clean`, which a person invokes. Close the report with the snapshot path |
| 101 | +and that one line. Do not start a removal run and do not propose exact paths for one. When |
| 102 | +`effective` was `false`, say instead that removal is disabled by the plugin's configuration. |
| 103 | + |
| 104 | +## Next |
| 105 | + |
| 106 | +/disk-hygiene:clean <target-directory> |
| 107 | + |
| 108 | +Run by a person when the findings warrant removal; it takes the same target, not this snapshot. |
| 109 | + |
| 110 | +## Gotchas |
| 111 | + |
| 112 | +- The argument-free probe is not one of the calls the engine-gate adjudicates, so a session in a |
| 113 | + permission mode that asks may hold it for a person. That is the "waiting for a person" stop in |
| 114 | + step 1, not a reason to skip the probe. **Claim:** a headless session in the default permission |
| 115 | + mode denies the probe and a `--bg` session parks it, because no hook adjudicates it. |
| 116 | + **Basis:** the three probe lanes on |
| 117 | + [#5516](https://github.com/melodic-software/claude-code-plugins/issues/5516) (headless default |
| 118 | + denied, `--bg` default parked, headless `bypassPermissions` denied), run on Claude Code 2.1.285; |
| 119 | + upstream behavior is in the |
| 120 | + [permission modes doc](https://code.claude.com/docs/en/permission-modes) and the |
| 121 | + [hooks doc](https://code.claude.com/docs/en/hooks). **As of:** 2026-09-30. **Recheck:** a Claude |
| 122 | + Code release changes how permission modes treat Bash calls no hook adjudicates. |
| 123 | +- Under an inline `--plugin-dir` load the probe can report `data_root` as `null` even though the |
| 124 | + plugin data directory exists. Stop as step 1 says; do not substitute a guessed path. **Claim:** the |
| 125 | + guard derives `data_root` only from the `<plugins>/cache/<marketplace>/<name>` install layout or a |
| 126 | + registered directory marketplace, so a checkout loaded inline reports `null`. **Basis:** the probe |
| 127 | + run from a checkout with neither reported `hook_python` resolved and `data_root: null` on Claude Code |
| 128 | + 2.1.285 (Linux); the derivation rule is in |
| 129 | + [safety-model.md](../clean/reference/safety-model.md), "Local-directory marketplace installs". |
| 130 | + **As of:** 2026-09-30. **Recheck:** the guard's data-root derivation changes, or a Claude Code |
| 131 | + release documents a plugin data path for `--plugin-dir` loads. |
| 132 | +- A hint match is discovery evidence only. Reporting "N hinted entries" as "N safe deletions" is the |
| 133 | + failure this skill exists to avoid. |
0 commit comments