Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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.8.2",
"version": "0.8.3",
"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, changed entries, and live-handle uncertainty fail closed.",
"author": {
"name": "Melodic Software",
Expand Down
23 changes: 23 additions & 0 deletions plugins/disk-hygiene/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,29 @@
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.8.3]

### Fixed

- **RETRACTS 0.8.2's PowerShell claim, which was wrong (#1195).** 0.8.2 documented that "PreToolUse guards
do not intercept PowerShell-tool commands" and scoped the PowerShell lane behind a preview caveat. A
fresh-session controlled test falsified that: a `Bash|PowerShell` PreToolUse matcher **does** fire for the
PowerShell tool on 2.1.218, the payload `tool_name` is literally `PowerShell`, and a live `Set-Content`
through that tool was blocked. There is no harness firing divergence and no preview limitation involved —
0.8.2's caveat overstated an un-isolated inference and is removed.
- **The real defect, now documented accurately: the plugin-level engine gate is inert whenever
`disk_hygiene_enabled` is unconfigured.** `hooks/hooks.json` passes a bare
`${user_config.disk_hygiene_enabled}`; upstream never implemented the declared userConfig `default`, so an
unset-but-defaulted token is neither substituted nor exported as `CLAUDE_PLUGIN_OPTION_*` and its presence
**drops the entire hook entry** (proven: token-carrying hooks vanish while token-free controls fire, and
return once the key is configured). So the gate has never run for any consumer who never set the key — on
Bash and PowerShell alike, which is the real shape of the reported "PowerShell bypass". The skill-scoped
belt carries no such token and is unaffected. Every doc that claimed the gate "fires in every session"
or that audit-only mode is "guard-enforced" corrected: the `clean` and `setup` `SKILL.md` files,
`reference/safety-model.md`, and the consumer `README.md`. The code fix (a delivery channel that does
not depend on the unimplemented `default`) is tracked separately.
Recheck when the upstream gap closes (#46477 / #39455 / #39827).

## [0.8.2]

### Fixed
Expand Down
47 changes: 30 additions & 17 deletions plugins/disk-hygiene/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,14 @@ at preview. Backups remain the recovery boundary for user data.
there, so treat the number printed here as a convenience copy). Claude Code launches the guard in
shell-free exec form; guarded engine calls must use the same absolute interpreter reported by that
guard, so Bash aliases and functions cannot replace it. The guard registers on two surfaces: a
plugin-level **engine gate** (`hooks/hooks.json`) that fires in every session but acts only on
commands referencing the engine — deferring everything else instantly — and enforces the
configured kill switch and data-root authority through plugin-hook substitution; and the
plugin-level **engine gate** (`hooks/hooks.json`) that acts only on commands referencing the
engine — deferring everything else instantly — and enforces the configured kill switch and
data-root authority through plugin-hook substitution. **Caveat (verified on Claude Code 2.1.218):**
that gate only registers once `disk_hygiene_enabled` is **explicitly configured**. Upstream never
implemented the declared userConfig `default`, so while the option is unset its
`${user_config.disk_hygiene_enabled}` argument is neither substituted nor exported, and its
presence **drops the whole hook entry** — the gate does not run at all, on either tool. The
skill-scoped belt below carries no such token and is unaffected. And the
skill-scoped **belt** inside the `clean` skill's context, which adds the deny-by-default Bash and
deletion-spelling PowerShell discipline during active cleanup work. Hook-lifetime caveat: docs
scope a skill hook to the component's lifetime, but session-long firing of the belt has been
Expand Down Expand Up @@ -148,21 +153,29 @@ hand-cleaning the zone.
- **MCP / external trust:** no MCP server, agent, dependency, or third-party service is shipped.
- **Configuration:** one non-sensitive `userConfig` boolean (`disk_hygiene_enabled`, default
`true`) gating the execution tiers — setting it `false` puts `/disk-hygiene:clean` in audit-only
mode. That mode is now guard-enforced: the plugin-level engine gate receives the configured value
by exec-form substitution and denies engine invocations outright when it is `false`, in every
session. The skill self-enforcement (kill-switch probe + skill-content value) and the skill-scoped
belt remain as redundant layers; the belt still cannot receive the value (skill-frontmatter hooks
get neither `${user_config.*}` substitution nor `CLAUDE_PLUGIN_OPTION_*`), and still forces a
human prompt before every mutation.
mode. When the value is **explicitly configured `false`**, the plugin-level engine gate receives it
by exec-form substitution and denies engine invocations outright. **Caveat (verified on Claude Code
2.1.218):** this holds only for a configured value — because upstream never implemented the declared
userConfig `default`, an *unset* `disk_hygiene_enabled` is neither substituted nor exported, and its
presence in the gate's args drops the whole hook, so on a default (unconfigured) install the engine
gate does not run at all. The skill self-enforcement (kill-switch probe + skill-content value) is
Comment thread
kyle-sexton marked this conversation as resolved.
therefore the primary kill-switch honoring on a default install, not a redundant layer; the
skill-scoped belt cannot receive the value either (skill-frontmatter hooks get neither
`${user_config.*}` substitution nor `CLAUDE_PLUGIN_OPTION_*`) and still forces a human prompt before
every mutation.
- **Trust-surface record (0.7.0):** the plugin-level `hooks/hooks.json` PreToolUse registration is a
NEW trust surface (a hook that launches in every consumer session), added deliberately for
guard-enforced audit-only mode and data-root authority (#1106 decision, Option E — split
registration). Its blast radius is bounded by design: exec form (no shell), bundled
standard-library script only, instant no-output deferral for any command not referencing the
engine, and no new capability beyond what the skill-scoped deployment already did during active
cleanup. Known costs, accepted: one `python3` launch per Bash/PowerShell call, and on a machine
where `python3` resolves to the Windows Store alias stub the launch fails on every call (tracked
with remediation detection in #1110). This entry is the plugin-acceptance review delta for the
NEW trust surface (a hook that launches in every consumer session **once `disk_hygiene_enabled` is
explicitly configured** — see the caveat below), added deliberately for guard-enforced audit-only
mode and data-root authority (#1106 decision, Option E — split registration). Its blast radius is
bounded by design: exec form (no shell), bundled standard-library script only, instant no-output
deferral for any command not referencing the engine, and no new capability beyond what the
skill-scoped deployment already did during active cleanup. Known costs, accepted: one `python3`
launch per Bash/PowerShell call **on a configured install**, and on a machine where `python3`
resolves to the Windows Store alias stub the launch fails on every call (tracked with remediation
detection in #1110). **Caveat (verified on Claude Code 2.1.218):** while `disk_hygiene_enabled` is
unset, the bare `${user_config.*}` argument drops the whole hook, so on a default install this hook
does not register or launch at all — neither the trust surface nor its per-call cost applies until
the option is configured. This entry is the plugin-acceptance review delta for the
change. A direct `hygiene.py` invocation outside that skill does not read the toggle and
answers only to the engine's own preview/approval-token gate. The toggle can only narrow the
destructive surface, never widen it (see [the safety model](skills/clean/reference/safety-model.md)
Expand Down
55 changes: 29 additions & 26 deletions plugins/disk-hygiene/skills/clean/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,15 +261,15 @@ handoff, not an engine plan:
4. Skip and report any path whose verdict is not `clear`; never substitute a sibling, retry
around a lock, or delete under a stale verdict.

The PowerShell guard lane is *designed* to turn deletion spellings into a final human permission prompt
(the same bar as the engine apply prompt); confirm that prompt only when the command matches the exact
approved list. Engine invocations from PowerShell are *designed* to stay hard-denied (also subject to
the caveat below). **Caveat (Claude Code 2.1.218, Windows): none of this PowerShell interception fires
for the PowerShell tool** — the deletion-spelling prompt AND the engine-invocation deny are both inert;
the PowerShell tool is in preview and
PreToolUse hooks were reproduced not to intercept PowerShell-tool commands, so on Windows do not rely on
this prompt; the manual lane's per-path `handoff-verify` approval and the baseline permission policy are
the protections that actually hold. See the PowerShell-preview gotcha below and `reference/safety-model.md`.
The PowerShell guard lane turns deletion spellings into a final human permission prompt (the same
bar as the engine apply prompt); confirm that prompt only when the command matches the exact
approved list. Engine invocations from PowerShell stay hard-denied. **Caveat — the plugin-level engine
gate is inert until `disk_hygiene_enabled` is explicitly configured (verified on Claude Code 2.1.218):**
that gate (`hooks/hooks.json`) passes a bare `${user_config.disk_hygiene_enabled}`, and an
unset-but-defaulted userConfig value drops the whole hook entry, so for any consumer who never set the
key the gate never runs — on the Bash tool and the PowerShell tool alike. The skill-scoped belt (this
skill's frontmatter hook) carries no such token and is unaffected; `Bash|PowerShell` PreToolUse hooks do
fire for the PowerShell tool. See `reference/safety-model.md`.

Summarize removed paths, logical bytes removed, observed free-space delta, and every skip grouped by
`locked`, `changed-or-link`, `protected`, `needs-elevation`, `handle-state-unverified`, or
Expand Down Expand Up @@ -299,10 +299,13 @@ sparse files, hard links, compression, and delayed allocation affect it.
apply shapes using the hook runtime's same absolute executable pass. Shell expansions, globs,
splitting/escape forms, operators, redirections, aliases, and exported functions fail closed.
- The guard registers twice: a plugin-level engine gate (`hooks/hooks.json`, `--mode engine-gate`)
that fires in every session, receives the kill switch and data root by plugin-hook substitution,
and defers instantly on any command not referencing the engine; and this skill's frontmatter belt,
which adds the deny-by-default Bash and deletion-spelling PowerShell discipline while cleanup is
the active work. Verdicts are idempotent where both fire.
that receives the kill switch and data root by plugin-hook substitution and defers instantly on any
command not referencing the engine; and this skill's frontmatter belt, which adds the deny-by-default
Bash and deletion-spelling PowerShell discipline while cleanup is the active work. Verdicts are
idempotent where both fire. **Caveat (verified on Claude Code 2.1.218):** the engine gate only
registers once `disk_hygiene_enabled` is explicitly configured — its bare `${user_config.*}` argument
drops the whole hook while the option is unset (upstream never implemented the declared `default`), so
on a default install only the frontmatter belt runs, and only during `clean`.
- The guard hook launches in exec form via `python3`, resolved on `PATH` with no shell (`python3`,
not bare `python`, because stock macOS and many Linux distros ship only `python3` and a legacy
`python` 2.x would crash the guard on modern syntax). Enforcement is therefore only as strong as
Expand All @@ -316,21 +319,21 @@ sparse files, hard links, compression, and delayed allocation affect it.
manual-handoff lane already requires and the consumer's baseline permission policy — defense-in-depth
lost, not preserved. `/disk-hygiene:setup check` reports whether the interpreter resolves on this
machine.
- **The PowerShell deletion belt does not fire for the PowerShell *tool* on current builds (Claude Code
2.1.218, Windows) — distinct from the `python3`-resolution loss above.** Even with `python3` resolving,
a `Bash|PowerShell` PreToolUse hook was reproduced to fire for the Bash tool but NOT intercept
PowerShell-*tool* commands. The PowerShell tool is a documented *preview* feature
([tools-reference](https://code.claude.com/docs/en/tools-reference)); PreToolUse interception of it is
not a listed preview limitation, so this is an observed docs-vs-behavior gap (mechanism not yet
isolated — matcher firing vs Windows payload delivery vs the tool's `tool_name`). Consequence on
Windows: the PowerShell deletion belt AND the `disk_hygiene_enabled` kill switch's reach into the
manual PowerShell lane are both inert; the protections that hold are the manual lane's per-path
`handoff-verify` approval and the consumer's baseline permission policy. Recheck when the PowerShell
tool exits preview or when interception is verified directly.
- **The plugin-level engine gate is dropped whenever `disk_hygiene_enabled` is unconfigured (Claude Code
Comment thread
kyle-sexton marked this conversation as resolved.
2.1.218) — distinct from the `python3`-resolution loss above.** `hooks/hooks.json` passes a bare
`${user_config.disk_hygiene_enabled}`; a declared userConfig `default` is not implemented upstream, so an
unset-but-defaulted token is neither substituted nor exported to `CLAUDE_PLUGIN_OPTION_*` and its presence
**drops the whole hook entry**. Fresh-session controlled test: token-carrying hooks vanish while token-free
controls fire, and return once the key is configured. Consequence: for any consumer who never set the key,
this gate has never run — on Bash and PowerShell alike. The skill-scoped belt carries no such token and is
unaffected. Recheck when the upstream `default` gap closes (#46477 / #39455 / #39827).
- **PreToolUse hooks DO fire for the PowerShell tool** (2.1.218; payload `tool_name` is literally
`PowerShell`, confirmed by a live block through that tool). A `Bash|PowerShell` matcher is correct and
there is no harness firing divergence — read `tool_name` from the stdin payload, not from an env var
(`CLAUDE_TOOL_NAME` does not exist).
- The PowerShell lane is the inverse tradeoff: it stays open for read-only support work (git, gh,
metadata probes) and instead hard-denies engine invocations and turns known deletion spellings
into a final human permission prompt (**subject to the preview caveat above — this does not fire for
the PowerShell tool on 2.1.218**). It is a raised bar, not a fail-closed lane; the engine's
into a final human permission prompt. It is a raised bar, not a fail-closed lane; the engine's
own containment and the Bash lane remain the deletion authority.
- The guard rejects `~` anywhere in a Bash command as a shell-expansion character, which includes
Windows 8.3 short names (`SOMEUS~1`). Always pass long-form paths; the guard's own disclosures
Expand Down
Loading