Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
467fe90
feat(disk-hygiene): add deep-inventory categories and KEEP-reason val…
kyle-sexton Sep 30, 2026
d913856
feat(disk-hygiene): add read-only inventory subcommand with deep mode
kyle-sexton Sep 30, 2026
c598437
test(disk-hygiene): make inventory tests host-independent and cache o…
kyle-sexton Sep 30, 2026
9d342c5
docs(disk-hygiene): document attended deep inventory and add repo-hyg…
kyle-sexton Sep 30, 2026
7004acb
Merge remote-tracking branch 'origin/main' into feat/5221-disk-hygien…
kyle-sexton Sep 30, 2026
13f2833
chore(disk-hygiene): bump to 0.30.0 and repo-hygiene to 0.15.1 with c…
kyle-sexton Sep 30, 2026
fc25ef3
fix(disk-hygiene): report UNKNOWN when the process table was not read
kyle-sexton Sep 30, 2026
71e3f04
Merge origin/main into feat/5221-disk-hygiene-deep-inventory
kyle-sexton Sep 30, 2026
0e81396
Merge origin/main into feat/5221-disk-hygiene-deep-inventory
kyle-sexton Sep 30, 2026
d0f85a3
docs(disk-hygiene): document --deep in the README and scope the /tmp …
kyle-sexton Sep 30, 2026
9b104f3
Merge origin/main into feat/5221-disk-hygiene-deep-inventory
kyle-sexton Sep 30, 2026
e144394
fix(disk-hygiene): reject an empty KEEP reason that only carries evid…
kyle-sexton Sep 30, 2026
c8d7817
Merge remote-tracking branch 'origin/main' into feat/5221-disk-hygien…
kyle-sexton Sep 30, 2026
7a98d81
Merge origin/main into feat/5221-disk-hygiene-deep-inventory
kyle-sexton Sep 30, 2026
d84259b
Merge origin/main into feat/5221-disk-hygiene-deep-inventory
kyle-sexton Sep 30, 2026
006cfe6
Merge origin/main into feat/5221-disk-hygiene-deep-inventory
kyle-sexton Sep 30, 2026
6e852df
Merge origin/main into feat/5221-disk-hygiene-deep-inventory
kyle-sexton Sep 30, 2026
b22f973
Merge origin/main into feat/5221-disk-hygiene-deep-inventory
kyle-sexton Sep 30, 2026
3b8e1dd
Merge branch 'main' into feat/5221-disk-hygiene-deep-inventory
kyle-sexton Sep 30, 2026
619360e
fix(disk-hygiene): stop the inventory at bind mounts, count open file…
kyle-sexton Sep 30, 2026
84156c9
Merge branch 'main' into feat/5221-disk-hygiene-deep-inventory
kyle-sexton Sep 30, 2026
19eb0aa
fix(disk-hygiene): report an unreadable mountinfo in the inventory su…
kyle-sexton Sep 30, 2026
2fb30c0
Merge remote-tracking branch 'origin/main' into feat/5221-disk-hygien…
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
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.38.0",
"version": "0.39.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
19 changes: 19 additions & 0 deletions plugins/disk-hygiene/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,25 @@
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.39.0] - 2026-09-30

### Added

- **Read-only `inventory` subcommand with a deep mode**
([#5221](https://github.com/melodic-software/claude-code-plugins/issues/5221)). `hygiene.py inventory
--target <path> [--data-root <dir>] [--deep]` lists what is under a target and writes a JSONL report and
a JSON summary under `<data-root>/inventory/`. Deep mode adds per-category entries, each with a
validated KEEP reason (exit 5 when the validator fails); without a readable `/proc`, rows that depend
on the process table are UNKNOWN, not CANDIDATE. A home-directory target, or any target with
`--deep`, runs it before any `scan`, so a bare `/disk-hygiene:clean ~` starts there. The engine
grammar declares the subcommand read-only, so the destructive guard admits it beside `catalog`.
A superseded version a symlink points at is kept, a release outranks its own prerelease, a
plugin cache candidate carries its `.orphaned_at` marker age and sweep-window flag, and the
`tmp-producer` category reads `/tmp`, not `$TMPDIR`. The walk does not enter a bind mount on the
same device (read from `/proc/self/mountinfo`), an open file counts as use of a `/tmp` entry, and
the report is written to a temporary file and renamed only when the walk finishes.
`skills/clean/SKILL.md` and its references document the attended workflow.

## [0.38.0] - 2026-09-30

### Added
Expand Down
6 changes: 6 additions & 0 deletions plugins/disk-hygiene/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,12 @@ gets the relaxed directory listing.
confirmation as an unbounded walk, sums through VCS and protected directories read-only, and has no
entry cap.

`--deep`, and a home-directory target without it, runs the read-only deep inventory before any
scan: every entry with its producer, a disposition and a reason, where each `KEEP` names who
produced the entry and what still uses it. It reports only and prepares no deletion; removing
anything it lists still goes through scan, preview and the removal approval. Columns and
categories: `skills/clean/reference/scan-flags.md`.

The skill stores snapshots, plans, and reports under `${CLAUDE_PLUGIN_DATA}`. It never writes generated
state into the installed plugin directory or the audited target.

Expand Down
20 changes: 20 additions & 0 deletions plugins/disk-hygiene/lib/engine_grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,26 @@ def _data_root_flag() -> Flag:
),
help="inventory a target without mutating it",
),
Subcommand(
"inventory",
(
Flag("--target", required=True, example="target-dir"),
_data_root_flag(),
Flag(
"--deep",
takes_value=False,
help=(
"list every level of the target instead of its immediate "
"children; the default when the target is the user's home "
"directory"
),
),
),
help=(
"report each entry's producer, disposition and reason; writes a "
"report that preview and apply never accept"
),
),
Subcommand(
"preview",
(
Expand Down
29 changes: 14 additions & 15 deletions plugins/disk-hygiene/skills/clean/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: "Audit an arbitrary directory tree for orphaned, temporary, stale-lock, failed-write, partial-download, and empty leftover artifacts; classify evidence into confidence tiers; and optionally remove exact validated paths after explicit per-tier approval. Read-only by default and manual-only. Use when: 'audit this directory', 'find orphaned files', 'what junk can I clean up', 'reclaim disk space', 'find temp or lock leftovers', 'clean up my home directory'. Skip when: repository cache/build cleanup belongs to repo-hygiene, a product has its own prune/GC command, or the target is an OS-managed root."
argument-hint: "[--execute] [--max-depth <N>] [--sizes-only] [--policy <file>] [options] <target-directory>"
argument-hint: "[--execute] [--deep] [--max-depth <N>] [--sizes-only] [--policy <file>] [options] <target-directory>"
user-invocable: true
disable-model-invocation: true
hooks:
Expand All @@ -27,7 +27,7 @@ metadata:
summary: Audit a directory tree for stale leftovers and remove validated paths
---

**Arguments.** `[--execute] [--max-depth <N>] [--sizes-only] [--policy <file>] [options] <target-directory>`. Full form: `[--execute] [--policy <policy.json>] [--max-depth <N>] [--confirmed-large-scan] [--sizes-only] [--quiet] [--root-children [--root-child <name>]...] <target-directory>`
**Arguments.** `[--execute] [--deep] [--max-depth <N>] [--sizes-only] [--policy <file>] [options] <target-directory>`. Full form: `[--execute] [--deep] [--policy <policy.json>] [--max-depth <N>] [--confirmed-large-scan] [--sizes-only] [--quiet] [--root-children [--root-child <name>]...] <target-directory>`

# Disk hygiene

Expand All @@ -41,8 +41,7 @@ optional execution lane. On Windows and macOS a run ends in a report plus the `e

## Arguments and boundaries

Parse `$ARGUMENTS` as the complete user-facing surface: optional `--execute`, optional
`--policy <file>`, optional `--max-depth <N>`, optional `--confirmed-large-scan`, optional
Parse `$ARGUMENTS` as the complete user-facing surface: optional `--execute`, optional `--deep` ([deep inventory](#deep-inventory)), optional `--policy <file>`, optional `--max-depth <N>`, optional `--confirmed-large-scan`, optional
`--quiet`, optional `--root-children` with zero or more `--root-child <name>`, and one target
directory. Remaining engine flags (`--output`, `--project-dir`, `--data-root` on scan;
`--snapshot`, `--plan`, `--report`, `--confirm-tier`, `--approval-token`, `--paths`, `--path`, and
Expand Down Expand Up @@ -158,10 +157,13 @@ naming what the question never presented cannot be met.
| Root-children selection (`--root-children`, §1) | one or more admitted immediate children just listed (directories, or regular files on a volume root), never "everything" or the scan target itself |
| Removal approval (§5) and manual handoff (§6) | exactly the one tier and the exact path list just shown |

**`--sizes-only`** goes through the same large-scan question as an ordinary unbounded walk, so a
known-large root needs `--max-depth` or `--confirmed-large-scan`; it sums through VCS and protected
directories, read-only, keeps no per-path entries, and has no entry cap. Detail:
[scan-flags.md](reference/scan-flags.md#--sizes-only).
**`--sizes-only`** goes through the same large-scan question as an ordinary unbounded walk, so a known-large root needs `--max-depth` or `--confirmed-large-scan`; it sums through VCS and protected directories, read-only, keeps no per-path entries, and has no entry cap. Detail: [scan-flags.md](reference/scan-flags.md#--sizes-only).

## Deep inventory

A home-directory target, or any target with `--deep`, starts with the read-only `inventory` subcommand (`hygiene.py inventory --target <target> --data-root <data-root> [--deep]`).
A bare `/disk-hygiene:clean ~` runs it before any `scan`. It asks no question and passes no confirmation-gate row. Present its report grouped by category, `CANDIDATE` rows first and `UNKNOWN` rows as coverage gaps.
It only reports: removing a listed entry still takes `scan`, a fresh `preview` and the removal approval, and every `KEEP` row needs a specific reason ([safety-model.md](reference/safety-model.md#deep-inventory-is-report-only), [scan-flags.md](reference/scan-flags.md#--deep)).

## 1. Create a read-only snapshot

Expand All @@ -177,18 +179,15 @@ or `${CLAUDE_PLUGIN_ROOT}`. Run:
[--root-children [--root-child <name>]...]
```

For exact per-child byte totals without paying for a per-entry inventory (or the entry cap), add
`--sizes-only` (a known-large target still needs `--confirmed-large-scan` or `--max-depth`). The snapshot carries `inventory_mode: sizes-only` and `rollup_precision: exact`
when every subtree was walked; a depth cut, a directory that failed to scan, or a mount-state error
marks `rollup_precision: partial`. Entry-cap error and next steps: [scan-flags.md](reference/scan-flags.md).
For exact per-child byte totals without a per-entry inventory or the entry cap, add `--sizes-only` (a known-large target still needs `--confirmed-large-scan` or `--max-depth`; [snapshot fields, entry-cap next steps](reference/scan-flags.md#--sizes-only)).
Pasteable fan-out worker instructions: [fan-out-worker-brief.md](reference/fan-out-worker-brief.md).

The guard validates `--data-root` against the plugin data directory it derives itself, and denies
the call outright when it cannot recognize the install layout, so a run reporting that denial is a
coverage gap, not a clean result. (Derivation and its fail-closed rationale: `reference/safety-model.md`.)

For a large root (a home directory, anything whose recursive walk could exceed the engine's entry cap),
start with a bounded pass: add `--max-depth 1` to inventory the target's loose files and immediate children,
a `scan` starts with a bounded pass (a home directory gets the [deep inventory](#deep-inventory) first): add `--max-depth 1` to inventory the target's loose files and immediate children,
then fan out deeper scans per subtree that the evidence justifies. After that depth-1 pass, re-inventory
the directories the operator approved with `--root-children` and one `--root-child <name>` per approved
immediate child: one snapshot, paths relative to the original target, no whole-home walk. The engine backs this with a
Expand Down Expand Up @@ -466,8 +465,8 @@ and what the guard does when no Python resolves → "Hook launch form".
snapshot token exists.
- `allowed-tools` would pre-approve rather than restrict tools, so this destructive skill intentionally
grants none. Consumer permission policy remains authoritative.
- The Bash lane is deny-by-default: only the literal-word bundled scan, preview, handoff-verify,
catalog, apply, and handoff-apply shapes (plus the argument-free kill-switch probe) pass, using the hook
- The Bash lane is deny-by-default: only the literal-word bundled scan, inventory, preview,
handoff-verify, catalog, apply, and handoff-apply shapes (plus the argument-free kill-switch probe) pass, using the hook
runtime's own absolute interpreter. The same denial text also admits literal-form read-only
supporting commands whose heads are absolute paths under a trusted system directory: `[`,
`basename`, `dirname`, `du`, `file`, `find`, `ls`, `pwd`, `stat`, `test` (`[` only as a complete
Expand Down
26 changes: 26 additions & 0 deletions plugins/disk-hygiene/skills/clean/evals/evals.json
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,32 @@
"Does not count the image's bytes as reclaimable and shows the virtual-disk size qualifier",
"Does not offer to delete the image; shrinking or removing it belongs to the owning hypervisor or WSL tooling"
]
},
{
"id": 16,
"name": "deep-inventory-lists-with-justified-keep-and-deletes-nothing",
"prompt": "/disk-hygiene:clean --deep ~ — list everything in my home directory and tell me what is safe to remove.",
"expected_output": "Runs the read-only inventory subcommand in deep mode and presents a report grouped by category with CANDIDATE rows first. Every KEEP row carries a specific reason. Nothing is deleted; removal of any candidate goes through scan, a fresh preview, and one-tier approval.",
"files": [],
"expectations": [
"Runs hygiene.py inventory with --deep and does not run apply",
"Rows use the shared schema columns including producer, category, disposition, reason, and evidence",
"Gives every KEEP row a specific reason and does not accept a bare category phrase such as 'tool-managed' without evidence the tool still references the entry",
"Treats a CANDIDATE row as a finding, not approval: offers removal only through scan, preview, and the confirmation gate's one-tier path list"
]
},
{
"id": 17,
"name": "bare-home-target-runs-deep-inventory-first",
"prompt": "/disk-hygiene:clean ~",
"expected_output": "A home-directory target with no flags starts with the read-only inventory subcommand, deep by default there, before any scan, and presents its report grouped by category with CANDIDATE rows first. It does not open with a bounded --max-depth 1 scan, asks no question, and deletes nothing.",
"files": [],
"expectations": [
"Runs hygiene.py inventory --target for the home directory without needing --deep, and runs it before any scan",
"Does not open with scan --max-depth 1 or the large-scan question",
"Does not run apply, prepare an approval, or treat a CANDIDATE row as a tier",
"Presents the report grouped by category, CANDIDATE rows first and UNKNOWN rows as coverage gaps"
]
}
]
}
17 changes: 17 additions & 0 deletions plugins/disk-hygiene/skills/clean/reference/safety-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- [Trust boundaries](#trust-boundaries)
- [Tidiness, not emergency](#tidiness-not-emergency)
- [Deep inventory is report-only](#deep-inventory-is-report-only)
- [Non-overridable checks](#non-overridable-checks)
- [Live agent scratchpads](#live-agent-scratchpads)
- [Handle semantics and honest scope](#handle-semantics-and-honest-scope)
Expand Down Expand Up @@ -51,6 +52,22 @@ the current defaults rather than funding a proportionality rebuild. **As of:** 2
**Recheck:** reopening #3855, or a funded design that names which rule yields and under what
bounded conditions.

## Deep inventory is report-only

The `inventory` subcommand (the [deep inventory](../SKILL.md#deep-inventory) mode) is attended and
read-only. It writes only its own JSONL report and summary under the data root. Neither is a
snapshot or a plan, so `preview` refuses them and no approval token can derive from them. A
`CANDIDATE` disposition is a finding: it grants no tier, and the guard admits the subcommand
because it cannot mutate, not because its output authorizes anything.

Every `KEEP` row must carry a specific reason (who produced the entry and what still uses it). The
validator rejects an empty reason, and a bare category phrase unless it names a tool and `evidence`
shows that tool still references the entry. A rejected row fails the report with exit 5.

`--execute`, the low-signal rule (Low is kept unless the human separately reviews exact paths), and
every confirmation gate apply exactly as before. Removing anything the inventory lists goes through
`scan`, a fresh `preview`, and the removal approval.

## Non-overridable checks

- target containment; an OS-managed root (per `system_roots()`: the OS drive holding an existing
Expand Down
Loading
Loading