Skip to content

Commit 4e49cab

Browse files
kyle-sextonclaude
andauthored
feat(disk-hygiene): add model-invocable read-only audit skill (#5590)
Refs: #5516 ## Hold: do not merge The `do-not-merge` label is applied. Owner condition 4 on #5516 says to probe the guard `ask` on `apply --execute` under `bypassPermissions` before the skill ships. Lane c was not run: the auto-mode classifier denied the launch script and the denial was not worked around. Auto mode, this machine's default, was not probed either. Release the hold only after the owner runs lane c (`claude -p --permission-mode bypassPermissions --tools Bash`, same scratch target and prompt as lanes a and b) or accepts the gap in a comment here or on #5516. If lane c fails open, condition 4 says fall back to A2 for that lane. Second owner question: the argument-free kill-switch probe is not matched by the engine-gate filter, so a delegated audit in default mode stops at step 1 without a user allow rule for the probe. Accept that, or add the allow rule. Merge order: #5571 first, then this PR (see Related). ## Summary Adds `/disk-hygiene:audit`, a model-invocable, read-only skill that runs the kill-switch probe and one engine `scan` and reports the snapshot. It runs no `preview` or `apply`; removal stays a separate `/disk-hygiene:clean` run a person invokes. `clean` is unchanged. Ships together with #5571 (#5520). ## Fix - `skills/audit/SKILL.md` and `evals/evals.json` (three delegated-audit cases and one negative case where "clean up my disk / delete these" must route to `/disk-hygiene:clean`). - The scan template takes a `<project-dir>` placeholder (a literal absolute path, optional) in place of `${CLAUDE_PROJECT_DIR}`, and the skill tells the parent to replace every `${...}` token and `<placeholder>` in the fan-out worker brief with the probe's `hook_python` and `data_root` before spawning, since a worker cannot expand tokens. - The inline `--plugin-dir` `data_root: null` gotcha carries a claim, basis, as-of and recheck record. The null-`data_root` eval no longer expects a guard denial to relay: no engine call is submitted in that case. - `audit` is registered in `scripts/skill-leaf-name-registry.txt` with disk-hygiene as its 17th owner and a rationale paragraph. - README lists the skill; plugin 0.30.0 to 0.35.0 (main took 0.31.0 to 0.34.1 for other disk-hygiene changes while this PR was open) with a CHANGELOG entry linked to #5516; cheat sheet regenerated. - Audit-only (toggle off) still runs the read-only scan and drops the removal handoff, instead of stopping. The owner decision is silent on this; the owner may overrule. ## Verification Probe results (claude 2.1.285, worktree plugin loaded with `--plugin-dir`, `permission-mode default`, fresh scratch target): - Lane a, headless default: `apply --execute` denied by the guard (`exact-engine-apply`, ask); `staging.tmp` survived. - Lane b, `--bg` default: parked at a permission prompt on the apply; `staging.tmp` survived. - Lane c, `bypassPermissions`: not probed. Auto mode (this machine's default) not probed. - The argument-free probe is not matched by the engine-gate filter (`Bash(*hygiene.py*)`), so a default-mode model-invoked session needs a user approval or allow rule to run it. Under `--plugin-dir` the probe reports `data_root` null. Checks run locally on a9f61a8 (after merging origin/main and renumbering 0.34.0 to 0.35.0), all exit 0: `check-skill-leaf-names.sh --check` and its `.test.sh`; `check-adr-numbers.sh --check`; `check-changelog-parity.sh --check`, `--check-order`, `--check-bump origin/main`; `validate-plugins.sh`; `check-changed-skills.sh origin/main`; `check-skill-count-claims.sh`; `check-spoke-plugin-root.sh --check`; `check-docs-naming.sh --check`; `check-orphaned-fixtures.sh --check`; `generate-cheatsheet.mjs --check`; `check-evals-quality.sh` and `json.tool` on the audit evals; `hygiene.test.sh` (592 tests, 1 skipped) and `kill_switch_probe.test.sh`. The earlier "all pass" was wrong: on 4c4faf7 CI `lint-2` failed `skill-leaf-names` (fixed by the registry entry) and `adr-numbers` (a duplicate 0042 on main, fixed by main's renumber, which this branch now includes). The A2 fallback and the final call on closing #5516 stay with the owner, so this PR uses `Refs`, not `Closes`. ## Related #5516; #5520 / #5571 (ships together). Merge order: #5571 lands first. This PR links its worker brief, which still shows `${CLAUDE_PLUGIN_DATA}` and `${CLAUDE_PROJECT_DIR}` tokens until #5571 lands. #5571 is stale: its base predates main's 0.29.2 through 0.31.0 changelog entries, so it needs a rebase and a renumber above main's current version before it can merge. disk-hygiene versions on main keep moving, so before this PR merges, merge main and re-run `scripts/check-changelog-parity.sh --check-bump origin/main`, renumbering above main's then-current version. The repo squash-merges only, so the branch's `bump to 0.30.0` commit subject does not reach main. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent fece6f7 commit 4e49cab

7 files changed

Lines changed: 214 additions & 2 deletions

File tree

‎docs/skill-cheat-sheet.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ owned by [docs/catalog-taxonomy.md](catalog-taxonomy.md).
224224
| [`/discipline:tighten-your-output`](../plugins/discipline/skills/tighten-your-output/SKILL.md) | `discipline` | Tighten prose and code. Fewer words, no semantic loss |
225225
| [`/discipline:use-your-skills`](../plugins/discipline/skills/use-your-skills/SKILL.md) | `discipline` | Map the task to available skills and invoke them instead of reinventing |
226226
| [`/discipline:wait-what`](../plugins/discipline/skills/wait-what/SKILL.md) | `discipline` | Re-pitch the message that did not land. Missing context added, plain register, project vocabulary |
227+
| [`/disk-hygiene:audit`](../plugins/disk-hygiene/skills/audit/SKILL.md) | `disk-hygiene` | Scan a directory tree for stale leftovers and report the evidence, read-only |
227228
| [`/disk-hygiene:clean`](../plugins/disk-hygiene/skills/clean/SKILL.md) | `disk-hygiene` | Audit a directory tree for stale leftovers and remove validated paths |
228229
| [`/docs-hygiene:audit-derivability`](../plugins/docs-hygiene/skills/audit-derivability/SKILL.md) | `docs-hygiene` | Judge whether a doc earns its existence or should become a pointer |
229230
| [`/docs-hygiene:audit-encapsulation`](../plugins/docs-hygiene/skills/audit-encapsulation/SKILL.md) | `docs-hygiene` | Find external citations reaching into a skill's private surfaces |

‎plugins/disk-hygiene/.claude-plugin/plugin.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
33
"name": "disk-hygiene",
4-
"version": "0.35.2",
4+
"version": "0.36.0",
55
"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 without the complete checkout evidence bundle, changed entries, and live-handle uncertainty fail closed.",
66
"author": {
77
"name": "Melodic Software",

‎plugins/disk-hygiene/CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
All notable changes to the `disk-hygiene` plugin are documented here. Format follows
44
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.
55

6+
## [0.36.0] - 2026-09-30
7+
8+
### Added
9+
10+
- **`/disk-hygiene:audit`, a model-invocable read-only scan**
11+
([#5516](https://github.com/melodic-software/claude-code-plugins/issues/5516)). A delegated or
12+
orchestrated session can run the probe and one engine `scan` and report the snapshot with its
13+
coverage gaps. It runs no `preview` or `apply` and hands any removal to `/disk-hygiene:clean`,
14+
which stays manual-only.
15+
616
## [0.35.2] - 2026-09-30
717

818
### Fixed

‎plugins/disk-hygiene/README.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,13 @@ launched.
233233
```text
234234
/disk-hygiene:clean <target-directory>
235235
/disk-hygiene:clean --policy <policy.json> <target-directory>
236+
/disk-hygiene:audit [--max-depth <N>] [--sizes-only] [--policy <policy.json>] <target-directory>
236237
```
237238

239+
`/disk-hygiene:audit` is the model-invocable, read-only counterpart for delegated or orchestrated
240+
scans. It runs the engine's `scan` subcommand only, reports the snapshot with coverage gaps, and
241+
removes nothing. Any removal is a separate `/disk-hygiene:clean` run that a person invokes.
242+
238243
`--root-children` with `--root-child <name>` inventories only the named immediate children of the
239244
target. That is required for an OS-managed volume root (the root itself is never walked) and is
240245
also how a depth-1 home audit re-inventories the directories the operator approved, without
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"skill_name": "audit",
3+
"evals": [
4+
{
5+
"id": 1,
6+
"name": "probe-first-then-scan-only",
7+
"prompt": "Delegate a disk-hygiene audit of ~/scratch-audit to a subagent and bring back the report.",
8+
"expected_output": "Runs the argument-free kill-switch probe first, takes hook_python and data_root from it, runs a single hygiene.py scan into a run directory under the data root, and reports the snapshot without removing anything.",
9+
"files": [],
10+
"expectations": [
11+
"Runs kill_switch_probe.py before any hygiene.py call",
12+
"Runs only the hygiene.py scan subcommand, with the absolute hook Python and --data-root taken from the probe",
13+
"Writes the snapshot under the data root's runs directory, not inside the target",
14+
"Deletes, moves, or edits nothing in the target"
15+
]
16+
},
17+
{
18+
"id": 2,
19+
"name": "null-data-root-stops",
20+
"prompt": "Run a disk-hygiene audit report on ~/scratch-audit. The probe reports data_root as null.",
21+
"expected_output": "Reports the audit as not run because the install layout proved no data root, and submits no engine call, so there is no guard denial to relay.",
22+
"files": [],
23+
"expectations": [
24+
"Submits no hygiene.py call",
25+
"Does not substitute a guessed or default data root",
26+
"States plainly that the audit did not run"
27+
]
28+
},
29+
{
30+
"id": 3,
31+
"name": "hints-are-not-verdicts-and-removal-is-a-handoff",
32+
"prompt": "Do an orchestrator scan of ~/scratch-audit for leftovers, then tell me what to delete.",
33+
"expected_output": "Reports the snapshot with coverage gaps and hints labeled as hints, ranks nothing for deletion, and names /disk-hygiene:clean, invoked by a person, as the only route to removal.",
34+
"files": [],
35+
"expectations": [
36+
"Reports walked:false rollups and truncated paths as coverage gaps, not as small or clean",
37+
"Does not present hinted entries as safe deletions or propose an exact path list for removal",
38+
"Ends by pointing to /disk-hygiene:clean for any removal and starts no removal itself"
39+
]
40+
},
41+
{
42+
"id": 4,
43+
"name": "cleanup-request-routes-to-clean-not-audit",
44+
"prompt": "Clean up my disk: delete these leftover temp files under ~/scratch-audit.",
45+
"expected_output": "Does not run the audit skill; routes the removal request to /disk-hygiene:clean, which a person invokes.",
46+
"files": [],
47+
"expectations": [
48+
"Does not invoke /disk-hygiene:audit",
49+
"Names /disk-hygiene:clean as the route for deleting or reclaiming space",
50+
"Removes nothing itself"
51+
]
52+
}
53+
]
54+
}

‎scripts/skill-leaf-name-registry.txt‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,16 @@ setup *
107107
# `extract-ssot`'s concern and not this plugin's. provenance is the plugin's
108108
# former name and keeps the leaf only as a deprecated shim whose `audit` stub
109109
# points to `attribution:audit`.
110-
audit ai-slop,attribution,claude-config,claude-memory,codebase-health,context-budget,github,instruction-placement,machine-health,mcp-tools,mutation-testing,overengineering,plugin-quality,provenance,repo-fleet-hygiene,testing
110+
# disk-hygiene joins on its own grounds. Its object, the leftovers in one
111+
# directory tree, is supplied by the namespace, and the contract is exactly the
112+
# verb's: bare invocation runs the engine's one read-only `scan` and reports the
113+
# snapshot, which is written under the plugin data directory and never into the
114+
# target. The skill has no override and no removal lane; removal is the sibling
115+
# `clean`, which stays manual-only, so a model-invocable leaf that promised
116+
# removal would contradict the verb table's `clean` meaning. `scan` is the
117+
# verb-table synonym but names the engine subcommand, not the product, a
118+
# findings report with a handoff to `clean` (#5516).
119+
audit ai-slop,attribution,claude-config,claude-memory,codebase-health,context-budget,disk-hygiene,github,instruction-placement,machine-health,mcp-tools,mutation-testing,overengineering,plugin-quality,provenance,repo-fleet-hygiene,testing
111120

112121
# Fixed verb meaning: deterministic pass/fail gate. skill-quality checks a
113122
# skill, toolchain checks a build. instruction-placement joins on its own

0 commit comments

Comments
 (0)