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
bdb7282
feat(disk-hygiene): add managed-state owner registry as data with sch…
kyle-sexton Sep 30, 2026
e1b3b93
docs(disk-hygiene): specify the managed-state report for registry mat…
kyle-sexton Sep 30, 2026
c68862f
test(disk-hygiene): prove the managed-state registry grants no approv…
kyle-sexton Sep 30, 2026
ab62747
Merge remote-tracking branch 'origin/main' into feat/4006-managed-sta…
kyle-sexton Sep 30, 2026
e4e9033
feat(disk-hygiene): release the read-only managed-state owner registr…
kyle-sexton Sep 30, 2026
5419f28
test(disk-hygiene): fence deletion and registry commands to the engin…
kyle-sexton Sep 30, 2026
7c98f39
test(disk-hygiene): close the argv, placeholder and launcher gaps in …
kyle-sexton Sep 30, 2026
dee6a22
fix(disk-hygiene): mark the owner-registry test executable
kyle-sexton Sep 30, 2026
9e64e25
Merge remote-tracking branch 'origin/main' into feat/4006-managed-sta…
kyle-sexton Sep 30, 2026
fdc395d
docs(disk-hygiene): the managed-state report does not show destructiv…
kyle-sexton Sep 30, 2026
bc164c0
fix(disk-hygiene): keep the clean skill under the 500-line cap
kyle-sexton Sep 30, 2026
f1095a8
Merge remote-tracking branch 'origin/main' into feat/4006-managed-sta…
kyle-sexton Sep 30, 2026
98415b1
test(disk-hygiene): guard the two engine deletion lanes, not one
kyle-sexton Sep 30, 2026
88e71d4
Merge remote-tracking branch 'origin/main' into feat/4006-managed-sta…
kyle-sexton Sep 30, 2026
797bb10
docs(disk-hygiene): give a registry match precedence over the native-…
kyle-sexton Sep 30, 2026
ee9661d
docs(disk-hygiene): scope the managed-state handoff text to the no-ma…
kyle-sexton Sep 30, 2026
0355a89
docs(disk-hygiene): name the lane that runs the managed-state report'…
kyle-sexton Sep 30, 2026
331e267
Merge remote-tracking branch 'origin/main' into feat/4006-managed-sta…
kyle-sexton Sep 30, 2026
36d3c17
Merge remote-tracking branch 'origin/main' into feat/4006-managed-sta…
kyle-sexton Sep 30, 2026
79dbe34
fix(disk-hygiene): answer review on the managed-state report and regi…
kyle-sexton Sep 30, 2026
e045609
Merge remote-tracking branch 'origin/main' into feat/4006-managed-sta…
kyle-sexton Sep 30, 2026
b8a3254
fix(disk-hygiene): resolve the executable for each invocation in a co…
kyle-sexton Sep 30, 2026
2964371
Merge remote-tracking branch 'origin/main' into feat/4006-managed-sta…
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.39.0",
"version": "0.40.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
15 changes: 15 additions & 0 deletions plugins/disk-hygiene/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@
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.40.0] - 2026-09-30

### Added

- **A read-only managed-state owner registry**
([#4006](https://github.com/melodic-software/claude-code-plugins/issues/4006)).
`skills/clean/reference/owner-registry.json` maps managed-state locations to the tool that owns
them, validated by `owner-registry.schema.json`, with `managed-state-report.md` specifying how a
registry match is reported. A match grants no approval and adds no delete path; the engine is
unchanged. The report neither shows nor runs a product-native destructive command; the registry
keeps each as data. Its presence check and read-only command run through the PowerShell tool or
the operator, because the skill's Bash guard denies them, and by the resolved application
executable so a profile alias cannot stand in. An absent tool suppresses the commands, not the
manual step. Each entry carries a verification record: claim, basis, as-of date, recheck trigger.

## [0.39.0] - 2026-09-30

### Added
Expand Down
5 changes: 3 additions & 2 deletions plugins/disk-hygiene/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ contract); it never follows links or recursively deletes an unvalidated tree.
- A live-handle preflight runs immediately before deletion. Windows uses an exclusive `CreateFile`
probe for every entry. Linux/macOS require `lsof`; absence, incomplete authority, or diagnostics
produce `handle_state_unverified` and block the tier. The plugin never elevates itself.
- Managed state is always a report-only handoff to the owning product's documented cleanup/GC command.
A dry-run result is evidence for the report, never authorization for this engine to remove it.
- Managed state with no registry match is always a report-only handoff to the owning product's
documented cleanup/GC command. A dry-run result is evidence for the report, never authorization for
this engine to remove it. A registry match follows `skills/clean/reference/managed-state-report.md`.
- The skill-scoped guard is a fail-closed allowlist. It permits only canonical bundled scan/preview
calls made from literal shell words, returns `ask` for the two exact mutating shapes, `apply` and
`handoff-apply`, and denies every other Bash command. Brace, tilde, parameter, command, arithmetic, process, word-splitting,
Expand Down
10 changes: 5 additions & 5 deletions plugins/disk-hygiene/skills/clean/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ blocked target, 3 when elevation is needed or filesystem state could not be veri
operator plainly that unpushed commits and untracked or ignored files in that checkout will be lost.
- For state owned by a package manager, plugin manager, browser, IDE, cloud-sync client, or similar
product, research its documented dry-run/prune/GC command and report the handoff. Managed state is
never eligible for this engine, even when a native dry-run calls it eligible.
never eligible for this engine, even when a native dry-run calls it eligible. A registry match: §4.
- Never install a dependency, close another process's handle, or disable a retention mechanism.
- While the scan output's `elevation` is `never` (the default), never elevate or trigger UAC/sudo.
Report `needs-elevation` or `handle-state-unverified` and stop that tier. With `uac-prompt`, on
Expand Down Expand Up @@ -268,7 +268,7 @@ For each hinted or suspicious entry, inspect enough neighboring content and meta
1. What created it? Prefer a manifest, log, documented naming contract, sibling structure, or owning
tool over an age/name guess.
2. Is the owner active? Check current process/tool state without killing, pausing, or modifying it.
3. Does the owning system provide cleanup or retention? Its dry-run result is authoritative.
3. Does the owning system provide cleanup or retention? Match `reference/owner-registry.json` `path_patterns` first (§4); its dry-run result is authoritative.
4. Could this be real work product, a resumable download, a backup, a dependency pinned by constraints,
or a shell/cloud-sync folder? If uncertain, keep it.
5. Is the evidence current for this exact path? Re-resolve every sibling independently; never
Expand Down Expand Up @@ -365,9 +365,9 @@ mix tiers:
}
```

For managed state, report the documented native command and its current dry-run result, but do not add
the path to an engine plan. Paths in an engine plan are unmanaged, snapshot-relative, exact,
non-overlapping, and never globs.
Managed state never enters an engine plan, whose paths are unmanaged, snapshot-relative, exact, non-overlapping,
and never globs. A registry match follows only `reference/managed-state-report.md`; its step 4 shows no destructive
Comment thread
kyle-sexton marked this conversation as resolved.
command. Other managed state: report the documented native command and its current dry-run result.

## 5. Preview, then ask

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Managed-state report

The report a managed-state registry match produces. The registry is
[owner-registry.json](owner-registry.json), validated by
[owner-registry.schema.json](owner-registry.schema.json). The engine's eligibility rules for
managed state stay as [the safety model](safety-model.md) states them.

## Report per registry match

1. **Owner.** The entry's `owner` and `id`, with the matched path. The match is a hint for an
owner claim, not proof of one.
2. **Tool presence.** Before any command, resolve the entry's `tool` to an application executable
in the lane named under [Who runs the probes](#who-runs-the-probes). Absent: status
`absent-tool`, and the report offers no command. `manual_step` is an action in the product's
own interface, not a command, so the report still shows it as information, noting that it
applies only if the product is installed. Not run: the report says presence is unverified and
runs nothing further.
3. **Read-only command.** When present and `read_only_command` is set, run it in that lane by the
resolved executable and capture its output into the report verbatim. A null command means the
product has none; the report shows `manual_step` as information.
4. **Destructive native command.** Neither shown nor run by the report. The registry keeps each one
as data to inspect. The engine blocks a plan that claims a registry owner with
`native-managed-report-only` and issues no approval token for it. Whether the report may show a
destructive command, or offer one behind the engine's tier and exact-list approval, is the
owner's decision, and no route for either is built.
5. **Unmatched paths.** A managed-looking path with no registry match is reported as a coverage
gap. It is never `clean` and never removable.

## Who runs the probes

Steps 2 and 3 are tool calls or operator actions, never shipped code. The clean skill's Bash guard
denies both, since neither is a bundled engine shape or a listed supporting command (see the Bash
and PowerShell lane bullets under [Gotchas](../SKILL.md#gotchas)). Where the session has the
PowerShell tool, run them there (`Get-Command <tool> -CommandType Application`, then the command): that lane is open for
read-only support work and the guard gives those commands no decision, so the session's ordinary
permissions, and in auto mode its classifier, still decide. A profile alias or function can shadow
a tool's name, so resolve with `Get-Command <tool> -CommandType Application`, run the command as
`& '<Source>' <arguments>` with that resolved path, once for each `;`-separated invocation in a
compound command (`pulumi about; pulumi plugin ls` is two), and treat a name that resolves only to an alias
or function as `absent-tool`. Otherwise the operator runs both
outside the session, presence check first, and the report records what they paste. A probe nobody
ran is reported as not run, never as a result.

## Design check

| Design constraint | How this report meets it |
|---|---|
| Containment is untouched | The report adds no deletion capability; engine eligibility is unchanged. |
| Read-only and destructive are different gates | Step 3 is a tool call in the PowerShell lane or an operator action, never the engine and never shipped code. Step 4 shows and runs no destructive command, so none is offered outside the engine's approval; offering one behind that approval is not built. The test fence names both kinds of command, so a shipped probe runner would be a reviewed change to that fence. |
| Tool presence is checked first | Step 2 runs before step 3, in the same lane; absent gives `absent-tool` and no commands; the manual step, which is not a command, stays as information. |
| An entry is a hint, never authorization | Step 1 treats a match as a claim to prove. |
| Unmatched stays a coverage gap | Step 5. |
| The registry is inspectable | Plain JSON plus a schema, readable without running anything. |

## Scope

No destructive command is built. Whether a product-native destructive command is ever run stays
the owner's decision.
138 changes: 138 additions & 0 deletions plugins/disk-hygiene/skills/clean/reference/owner-registry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
{
"version": 1,
"note": "An entry is a hint for an owner claim, never authorization. A path matching an entry is a starting point for proving the owning product manages it, not proof. Commands are data for an operator to read; nothing here runs them. Paths are relative to the home directory. A null command means the product has none; manual_step names the in-app or settings action instead.",
"entries": [
{
"id": "docker-desktop",
"owner": "Docker Desktop",
"path_patterns": {
"windows": ["AppData/Local/Docker"],
"macos": ["Library/Containers/com.docker.docker"]
},
"tool": "docker",
"read_only_command": "docker system df",
"destructive_native_command": "docker image prune; docker builder prune",
Comment thread
kyle-sexton marked this conversation as resolved.
"manual_step": null,
"platforms": ["windows", "macos"],
"verification": {
"claim": "docker system df reports reclaimable space; docker image prune and docker builder prune are the owner's removal commands; the two data paths were seen on a Windows and a macOS host",
"basis": [
"https://docs.docker.com/reference/cli/docker/system/df/",
"https://docs.docker.com/reference/cli/docker/image/prune/",
"https://docs.docker.com/reference/cli/docker/builder/prune/",
"operator probe of the data paths recorded in issue 4006, no official page found"
],
"as_of": "2026-09-30",
"recheck": "a Docker CLI release note that renames or removes one of the three commands, or a Docker Desktop release note that moves its data directory"
}
},
{
"id": "nvidia-shader-cache",
"owner": "NVIDIA driver shader cache",
"path_patterns": {
"windows": ["AppData/Local/NVIDIA"]
},
"tool": "nvidia-smi",
"read_only_command": null,
"destructive_native_command": null,
"manual_step": "Manual step in Windows Disk Cleanup: select DirectX Shader Cache",
"platforms": ["windows"],
"verification": {
"claim": "the NVIDIA shader cache sits under the listed Windows path and Windows Disk Cleanup offers a DirectX Shader Cache item",
"basis": ["operator probe recorded in issue 4006, no official page fetched"],
"as_of": "2026-09-30",
"recheck": "an NVIDIA driver release note that moves the shader cache, or a Windows build whose Disk Cleanup drops the DirectX Shader Cache item"
}
},
{
"id": "cursor",
"owner": "Cursor",
"path_patterns": {
"windows": ["AppData/Roaming/Cursor"],
"macos": ["Library/Application Support/Cursor"],
"linux": [".config/Cursor"]
},
"tool": "cursor",
"read_only_command": null,
"destructive_native_command": null,
"manual_step": "In-app cache clearing in Cursor",
"platforms": ["windows", "macos", "linux"],
"verification": {
"claim": "Cursor keeps its state under the listed per-platform user-data paths and offers in-app cache clearing",
"basis": ["operator probe recorded in issue 4006, no official page fetched"],
"as_of": "2026-09-30",
"recheck": "a Cursor changelog entry that moves the user-data directory or removes in-app cache clearing"
}
},
{
"id": "openai-codex-cli",
"owner": "OpenAI Codex CLI",
"path_patterns": {
"windows": [".codex"],
"macos": [".codex"],
"linux": [".codex"]
},
"tool": "codex",
"read_only_command": null,
"destructive_native_command": null,
"manual_step": "Adjust Codex history retention in its configuration",
"platforms": ["windows", "macos", "linux"],
"verification": {
"claim": "Codex keeps user configuration under ~/.codex and config.toml offers history.max_bytes and history.persistence",
"basis": [
"https://learn.chatgpt.com/docs/config-file/config-basic",
"https://learn.chatgpt.com/docs/config-file/config-reference"
],
"as_of": "2026-09-30",
"recheck": "a Codex release note that changes the config directory or the history.* keys"
}
},
{
"id": "chezmoi",
"owner": "chezmoi",
"path_patterns": {
"windows": [".cache/chezmoi"],
"macos": [".cache/chezmoi"],
"linux": [".cache/chezmoi"]
},
"tool": "chezmoi",
"read_only_command": "chezmoi doctor",
"destructive_native_command": null,
"manual_step": null,
"platforms": ["windows", "macos", "linux"],
"verification": {
"claim": "chezmoi's default cacheDir is ~/.cache/chezmoi on every listed platform and chezmoi doctor checks for problems",
"basis": [
"https://www.chezmoi.io/reference/configuration-file/variables/",
"https://www.chezmoi.io/reference/commands/doctor/"
],
"as_of": "2026-09-30",
"recheck": "a chezmoi release note that changes the default cacheDir or the doctor command"
}
},
{
"id": "pulumi",
"owner": "Pulumi",
"path_patterns": {
"windows": [".pulumi"],
"macos": [".pulumi"],
"linux": [".pulumi"]
},
"tool": "pulumi",
"read_only_command": "pulumi about; pulumi plugin ls",
"destructive_native_command": "pulumi plugin rm <unused version>",
"manual_step": null,
"platforms": ["windows", "macos", "linux"],
"verification": {
"claim": "the plugin cache lives under ~/.pulumi by default, pulumi about prints environment information, pulumi plugin ls lists the cache, and pulumi plugin rm (alias of remove) deletes cached plugins",
"basis": [
"https://www.pulumi.com/docs/iac/cli/commands/pulumi_plugin_ls/",
"https://www.pulumi.com/docs/iac/cli/commands/pulumi_plugin_rm/",
"https://www.pulumi.com/docs/iac/cli/commands/pulumi_about/"
],
"as_of": "2026-09-30",
"recheck": "a Pulumi CLI release note that changes the plugin cache location or the plugin ls, plugin rm or about commands"
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["version", "note", "entries"],
"additionalProperties": false,
"properties": {
"version": {"const": 1},
"note": {"type": "string", "minLength": 1},
"entries": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": [
"id", "owner", "path_patterns", "tool", "read_only_command",
"destructive_native_command", "manual_step", "platforms", "verification"
],
"additionalProperties": false,
"properties": {
"id": {"type": "string", "minLength": 1},
"owner": {"type": "string", "minLength": 1},
"path_patterns": {
"type": "object",
"minProperties": 1,
"propertyNames": {"enum": ["windows", "macos", "linux"]},
"additionalProperties": {
"type": "array",
"minItems": 1,
"items": {"type": "string", "minLength": 1}
}
},
"tool": {"type": "string", "minLength": 1},
"read_only_command": {"type": ["string", "null"]},
"destructive_native_command": {"type": ["string", "null"]},
"manual_step": {"type": ["string", "null"]},
"platforms": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {"enum": ["windows", "macos", "linux"]}
},
"verification": {
"type": "object",
"required": ["claim", "basis", "as_of", "recheck"],
"additionalProperties": false,
"properties": {
"claim": {"type": "string", "minLength": 1},
"basis": {
"type": "array",
"minItems": 1,
"items": {"type": "string", "minLength": 1}
},
"as_of": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$"},
"recheck": {"type": "string", "minLength": 1}
}
}
}
}
}
}
}
3 changes: 2 additions & 1 deletion plugins/disk-hygiene/skills/clean/reference/safety-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,8 @@ read-only, for exact totals), keeps no per-path entries, and has no entry cap. I

Managed state is engine-ineligible. Even current native dry-run evidence is recorded only as a
report-only handoff because this engine cannot independently authenticate the owning product's state
or cleanup contract.
or cleanup contract. The report each registry match produces is specified in
[managed-state-report.md](managed-state-report.md).

The baseline policy therefore ships no discovery hint for another product's managed state. A hint
for a class the engine will never act on tells the operator to look for residue the plugin has
Expand Down
33 changes: 33 additions & 0 deletions plugins/disk-hygiene/skills/clean/scripts/owner_registry.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Cross-platform contract wrapper for the owner-registry test suite.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# shellcheck source=../../../scripts/test-wrapper-lib.sh
source "$SCRIPT_DIR/../../../scripts/test-wrapper-lib.sh"

ENGINE="$SCRIPT_DIR/hygiene.py"
FLOOR=""
test_wrapper::floor_to FLOOR "$ENGINE"
if [[ -z "$FLOOR" ]]; then
echo "FAIL: could not parse MIN_PYTHON from $ENGINE" >&2
exit 1
fi

PYTHON=""
test_wrapper::interpreter_to PYTHON
if [[ -z "$PYTHON" ]]; then
echo "SKIP: Python ${FLOOR}+ not found" >&2
exit 0
fi

FLOOR_CHECK=""
test_wrapper::floor_check_to FLOOR_CHECK "$FLOOR"
"$PYTHON" -c "$FLOOR_CHECK" || {
echo "SKIP: Python ${FLOOR}+ required" >&2
exit 0
}
PYFILE=""
test_wrapper::python_file_to PYFILE "$SCRIPT_DIR/test_owner_registry.py"
"$PYTHON" -m unittest -v "$PYFILE"
Loading
Loading