Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c83e764
feat(disk-hygiene): add a model-invocable read-only audit skill
kyle-sexton Sep 30, 2026
20477d7
fix(disk-hygiene): audit bootstrap matches what the probe and gate do
kyle-sexton Sep 30, 2026
57efcd3
Merge remote-tracking branch 'origin/main' into feat/5516-disk-hygien…
kyle-sexton Sep 30, 2026
a260150
feat(disk-hygiene): list the audit skill, add a routing eval, bump to…
kyle-sexton Sep 30, 2026
4c4faf7
Merge remote-tracking branch 'origin/main' into feat/5516-disk-hygien…
kyle-sexton Sep 30, 2026
76d72b4
fix(disk-hygiene): register the audit leaf name and align the audit s…
kyle-sexton Sep 30, 2026
a416dae
Merge remote-tracking branch 'origin/main' into feat/5516-disk-hygien…
kyle-sexton Sep 30, 2026
e55bad6
Merge remote-tracking branch 'origin/main' into feat/5516-disk-hygien…
kyle-sexton Sep 30, 2026
0b5c88c
docs(disk-hygiene): narrow the audit skill's token warning to the two…
kyle-sexton Sep 30, 2026
8258383
Merge remote-tracking branch 'origin/main' into feat/5516-disk-hygien…
kyle-sexton Sep 30, 2026
d86c174
Merge remote-tracking branch 'origin/main' into feat/5516-disk-hygien…
kyle-sexton Sep 30, 2026
cdfe648
Merge remote-tracking branch 'origin/main' into feat/5516-disk-hygien…
kyle-sexton Sep 30, 2026
f408655
Merge remote-tracking branch 'origin/main' into feat/5516-disk-hygien…
kyle-sexton Sep 30, 2026
0c9a92d
Merge remote-tracking branch 'origin/main' into feat/5516-disk-hygien…
kyle-sexton Sep 30, 2026
a9f61a8
Merge remote-tracking branch 'origin/main' into feat/5516-disk-hygien…
kyle-sexton Sep 30, 2026
75bff7a
fix(disk-hygiene): audit skill passes literal probe values, not subst…
kyle-sexton Sep 30, 2026
45e88b5
Merge origin/main into feat/5516-disk-hygiene-audit-skill
kyle-sexton Sep 30, 2026
1c4ef37
fix(disk-hygiene): drop the bare-python denial claim from the audit b…
kyle-sexton Sep 30, 2026
15384a6
Merge origin/main into feat/5516-disk-hygiene-audit-skill
kyle-sexton Sep 30, 2026
3b060f5
Merge origin/main into feat/5516-disk-hygiene-audit-skill
kyle-sexton Sep 30, 2026
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
1 change: 1 addition & 0 deletions docs/skill-cheat-sheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ owned by [docs/catalog-taxonomy.md](catalog-taxonomy.md).
| [`/discipline:tighten-your-output`](../plugins/discipline/skills/tighten-your-output/SKILL.md) | `discipline` | Tighten prose and code. Fewer words, no semantic loss |
| [`/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 |
| [`/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 |
| [`/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 |
| [`/disk-hygiene:clean`](../plugins/disk-hygiene/skills/clean/SKILL.md) | `disk-hygiene` | Audit a directory tree for stale leftovers and remove validated paths |
| [`/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 |
| [`/docs-hygiene:audit-encapsulation`](../plugins/docs-hygiene/skills/audit-encapsulation/SKILL.md) | `docs-hygiene` | Find external citations reaching into a skill's private surfaces |
Expand Down
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.35.2",
"version": "0.36.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 without the complete checkout evidence bundle, changed entries, and live-handle uncertainty fail closed.",
"author": {
"name": "Melodic Software",
Expand Down
10 changes: 10 additions & 0 deletions plugins/disk-hygiene/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
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.36.0] - 2026-09-30

### Added

- **`/disk-hygiene:audit`, a model-invocable read-only scan**
([#5516](https://github.com/melodic-software/claude-code-plugins/issues/5516)). A delegated or
orchestrated session can run the probe and one engine `scan` and report the snapshot with its
coverage gaps. It runs no `preview` or `apply` and hands any removal to `/disk-hygiene:clean`,
which stays manual-only.

## [0.35.2] - 2026-09-30

### Fixed
Expand Down
5 changes: 5 additions & 0 deletions plugins/disk-hygiene/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,13 @@ launched.
```text
/disk-hygiene:clean <target-directory>
/disk-hygiene:clean --policy <policy.json> <target-directory>
/disk-hygiene:audit [--max-depth <N>] [--sizes-only] [--policy <policy.json>] <target-directory>
```

`/disk-hygiene:audit` is the model-invocable, read-only counterpart for delegated or orchestrated
scans. It runs the engine's `scan` subcommand only, reports the snapshot with coverage gaps, and
removes nothing. Any removal is a separate `/disk-hygiene:clean` run that a person invokes.

`--root-children` with `--root-child <name>` inventories only the named immediate children of the
target. That is required for an OS-managed volume root (the root itself is never walked) and is
also how a depth-1 home audit re-inventories the directories the operator approved, without
Expand Down
133 changes: 133 additions & 0 deletions plugins/disk-hygiene/skills/audit/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
---
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."
argument-hint: "[--max-depth <N>] [--sizes-only] [--policy <file>] <target-directory>"
user-invocable: true
disable-model-invocation: false
metadata:
workflow-stage: anytime
summary: Scan a directory tree for stale leftovers and report the evidence, read-only
---

**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>`

# Disk hygiene audit

Scan one directory tree and report what the snapshot shows. This skill runs the engine's `scan`
subcommand and nothing else, and it changes nothing in the target. A filename pattern is a
discovery hint, never proof that an entry is junk.

This skill carries no hooks of its own. Safety rests on the plugin-level engine-gate in
`hooks/hooks.json`, which checks every Bash or PowerShell call that names `hygiene.py`. Run no
engine subcommand other than `scan`, no shell command that writes, moves, or deletes under the
target, and no compound shell around an engine call. A subagent follows that contract itself, from
the worker brief in step 2.

## 1. Bootstrap

Run the argument-free probe first, before any engine call:

```text
"<hook-python>" "${CLAUDE_PLUGIN_ROOT}/skills/setup/scripts/kill_switch_probe.py"
```

Take `hook_python` and `data_root` from its one-line JSON. Every engine call needs the absolute
`<hook-python>` and `--data-root`; a bare `python3` is rejected. When `hook_python` is not yet
known, submit the probe once with bare `python`. The probe reports `hook_python` as the interpreter
it ran under, so that value is the guard's only if the guard's own interpreter ran it. The scan is
admitted only under the guard's interpreter: if it is denied for that reason, the denial names the
interpreter and ran nothing, so rerun the scan with the interpreter it names.

- `data_root` is `null`: the install layout proved no data root and the guard denies every engine
call. Report the audit as not run, submit no engine call, and stop.
- The probe call is denied or left waiting for a person: report the audit as not run and stop. Do
not scan without it.
- The guard's interpreter is older than `MIN_PYTHON` in
[`hygiene.py`](../clean/scripts/hygiene.py): stop with that declared prerequisite. An older
`hook_python` from a bare-`python` probe is not that finding; the scan's admission settles it.
- `effective` is `false` (audit-only): the scan still runs. State the configured value, and leave
out the removal handoff in step 4. On `degraded: true`, say the configured value could not be read.

## 2. Scan

Pick a unique run directory under `<data_root>/runs/`; the snapshot lives there, never in the
target. With no target, ask the person who invoked you once. Reject an OS-managed root, a missing
directory, a symlink, or a Windows reparse point.

```text
"<hook-python>" "${CLAUDE_PLUGIN_ROOT}/skills/clean/scripts/hygiene.py" scan \
--target "<target>" --output "<run-dir>/snapshot.json" \
--data-root "<data_root>" [--project-dir "<project-dir>"] \
[--policy "<policy.json>"] [--max-depth <N>] [--sizes-only] [--quiet] \
[--root-children [--root-child <name>]...]
```

`--project-dir` is optional; pass it, as a literal absolute path, when the consumer project has
standing policy files. Pass literal values only: the guard rejects shell expansion, so never pass
an environment-variable reference. What each flag does, including the large-target and volume-root rules, is in
[scan-flags.md](../clean/reference/scan-flags.md). For a home directory or another large target,
start with `--max-depth 1`, then scan the subtrees the evidence justifies. Never pass
`--confirmed-large-scan` on your own: an unbounded walk needs a person's answer.

For a subtree worker, the brief to paste into the spawn prompt is
[fan-out-worker-brief.md](../clean/reference/fan-out-worker-brief.md). Before spawning, replace every
`${...}` token and `<placeholder>` in it with the literal absolute value you hold from the probe
(`hook_python`, `data_root`) or from step 2: a worker cannot expand `${...}` tokens, and its data
root is the probe's `data_root`. A worker returns scan evidence only.

## 3. Read and report the snapshot

Report from the snapshot and the scan's stdout, and nothing the run did not observe.

- Lead with `children_rollup`. `walked: true` rows carry exact totals; `walked: false` rows carry
`null` aggregates and `unwalked_reasons`, so report them as coverage gaps, never as small or clean.
- Rank on `reclaimable_local_bytes`, not `logical_bytes`. An entry whose `size_qualifiers` is
non-empty stays out of any reclaimable total; state its bytes and reasons separately.
- Report `truncated_paths` (a count under `--quiet`, the list in the snapshot) and every scan error
as coverage gaps. A `large-target-confirmation-required` or `root-children-selection-required`
status names the next step, not a failure.
- Quote hint coverage as a rate: `hinted_entries` of `entries`, never "N findings".
- List protected entries separately, and surface an `os_autoclean` recommendation as the engine
states it. A scan does not assess live handles or elevation, so it yields no locked,
needs-elevation, or unverified entries: say those were not assessed, never that there are none.
- Give each hinted entry the evidence the snapshot holds (path, hint, `protected_reasons`,
`size_qualifiers`). A hint has no owner check behind it, so label the list hints for a person to
judge, not verdicts, and rank nothing for deletion. Empty directories stay visible.
- A non-zero exit is a real failure: 2 for an invalid or blocked target, 3 when elevation is needed
or filesystem state could not be verified. Report it and stop.

## 4. Hand off

Removal is `/disk-hygiene:clean`, which a person invokes. Close the report with the snapshot path
and that one line. Do not start a removal run and do not propose exact paths for one. When
`effective` was `false`, say instead that removal is disabled by the plugin's configuration.

## Next

/disk-hygiene:clean <target-directory>

Run by a person when the findings warrant removal; it takes the same target, not this snapshot.

## Gotchas

- The argument-free probe is not one of the calls the engine-gate adjudicates, so a session in a
permission mode that asks may hold it for a person. That is the "waiting for a person" stop in
step 1, not a reason to skip the probe. **Claim:** a headless session in the default permission
mode denies the probe and a `--bg` session parks it, because no hook adjudicates it.
**Basis:** the three probe lanes on
[#5516](https://github.com/melodic-software/claude-code-plugins/issues/5516) (headless default
denied, `--bg` default parked, headless `bypassPermissions` denied), run on Claude Code 2.1.285;
upstream behavior is in the
[permission modes doc](https://code.claude.com/docs/en/permission-modes) and the
[hooks doc](https://code.claude.com/docs/en/hooks). **As of:** 2026-09-30. **Recheck:** a Claude
Code release changes how permission modes treat Bash calls no hook adjudicates.
- Under an inline `--plugin-dir` load the probe can report `data_root` as `null` even though the
plugin data directory exists. Stop as step 1 says; do not substitute a guessed path. **Claim:** the
guard derives `data_root` only from the `<plugins>/cache/<marketplace>/<name>` install layout or a
registered directory marketplace, so a checkout loaded inline reports `null`. **Basis:** the probe
run from a checkout with neither reported `hook_python` resolved and `data_root: null` on Claude Code
2.1.285 (Linux); the derivation rule is in
[safety-model.md](../clean/reference/safety-model.md), "Local-directory marketplace installs".
**As of:** 2026-09-30. **Recheck:** the guard's data-root derivation changes, or a Claude Code
release documents a plugin data path for `--plugin-dir` loads.
- A hint match is discovery evidence only. Reporting "N hinted entries" as "N safe deletions" is the
failure this skill exists to avoid.
54 changes: 54 additions & 0 deletions plugins/disk-hygiene/skills/audit/evals/evals.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"skill_name": "audit",
"evals": [
{
"id": 1,
"name": "probe-first-then-scan-only",
"prompt": "Delegate a disk-hygiene audit of ~/scratch-audit to a subagent and bring back the report.",
"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.",
"files": [],
"expectations": [
"Runs kill_switch_probe.py before any hygiene.py call",
"Runs only the hygiene.py scan subcommand, with the absolute hook Python and --data-root taken from the probe",
"Writes the snapshot under the data root's runs directory, not inside the target",
"Deletes, moves, or edits nothing in the target"
]
},
{
"id": 2,
"name": "null-data-root-stops",
"prompt": "Run a disk-hygiene audit report on ~/scratch-audit. The probe reports data_root as null.",
"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.",
"files": [],
"expectations": [
"Submits no hygiene.py call",
"Does not substitute a guessed or default data root",
"States plainly that the audit did not run"
]
},
{
"id": 3,
"name": "hints-are-not-verdicts-and-removal-is-a-handoff",
"prompt": "Do an orchestrator scan of ~/scratch-audit for leftovers, then tell me what to delete.",
"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.",
"files": [],
"expectations": [
"Reports walked:false rollups and truncated paths as coverage gaps, not as small or clean",
"Does not present hinted entries as safe deletions or propose an exact path list for removal",
"Ends by pointing to /disk-hygiene:clean for any removal and starts no removal itself"
]
},
{
"id": 4,
"name": "cleanup-request-routes-to-clean-not-audit",
"prompt": "Clean up my disk: delete these leftover temp files under ~/scratch-audit.",
"expected_output": "Does not run the audit skill; routes the removal request to /disk-hygiene:clean, which a person invokes.",
"files": [],
"expectations": [
"Does not invoke /disk-hygiene:audit",
"Names /disk-hygiene:clean as the route for deleting or reclaiming space",
"Removes nothing itself"
]
}
]
}
11 changes: 10 additions & 1 deletion scripts/skill-leaf-name-registry.txt
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,16 @@ setup *
# `extract-ssot`'s concern and not this plugin's. provenance is the plugin's
# former name and keeps the leaf only as a deprecated shim whose `audit` stub
# points to `attribution:audit`.
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
# disk-hygiene joins on its own grounds. Its object, the leftovers in one
# directory tree, is supplied by the namespace, and the contract is exactly the
# verb's: bare invocation runs the engine's one read-only `scan` and reports the
# snapshot, which is written under the plugin data directory and never into the
# target. The skill has no override and no removal lane; removal is the sibling
# `clean`, which stays manual-only, so a model-invocable leaf that promised
# removal would contradict the verb table's `clean` meaning. `scan` is the
# verb-table synonym but names the engine subcommand, not the product, a
# findings report with a handoff to `clean` (#5516).
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

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