/disk-hygiene:clean audits an arbitrary directory tree for abandoned temporary files, stale locks,
failed atomic-write remnants, empty leftovers, and similar disk residue. It is a context-aware audit,
not a static delete list: bundled patterns are discovery hints only, and every finding needs evidence
that it is not work product. Safe tidiness is the primary objective; reclaimable bytes are a
secondary signal, so zero-byte and empty-directory residue stay visible in reports.
The default lane is read-only. Cleanup is available only through a fresh, exact-path preview followed by explicit approval of one confidence tier. The engine then rechecks every candidate before removing only the entries captured in the snapshot; it never follows links or recursively deletes an unvalidated tree.
- The side-effecting skill is manual-only (
disable-model-invocation: true). Automated, scheduled, remote, or otherwise unattended sessions audit and stop. - Confidence controls report ordering, never authorization. High, medium, and low each require a separate approval naming every path with provenance, what the entry is, why it is removable, and risk; logical / reclaimable byte counts come last and never drop empty directories from the ranking.
- Filesystem roots, mount targets, OS-managed roots on every Windows volume (unless addressed only
via
--root-childrenwith an explicit child selection), user shell-folder roots, VCS metadata/tracked content, mount points (including Linux bind mounts), every Windows reparse point, symlinks, entries changed since the scan, and paths outside the target are hard stops. These predicates cannot be disabled by policy. The sole VCS exception is a read-only manual-handoff evidence mode for an entire standalone Git checkout: it requires empty porcelain status, every local head SHA confirmed through the checkout's GitHub remote, every stash SHA present in an independent checkout (or no stashes), and the existing exact-path operator approval. Without all four, categorical protection remains. - A live-handle preflight runs immediately before deletion. Windows uses an exclusive
CreateFileprobe for every entry. Linux/macOS requirelsof; absence, incomplete authority, or diagnostics producehandle_state_unverifiedand 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.
- The skill-scoped guard is a fail-closed allowlist. It permits only canonical bundled scan/preview
calls made from literal shell words, returns
askfor the one canonical apply shape, and denies every other Bash command. Brace, tilde, parameter, command, arithmetic, process, word-splitting, filename, redirection, and operator syntax is rejected before argument parsing. - Deletion walks the validated snapshot bottom-up. New entries are not traversed; they make the
directory non-empty and therefore skipped. After captured children are removed, a directory is
reopened with
O_NOFOLLOW, checked empty through its descriptor, and matched by device, inode, and type immediately before descriptor-relativermdir. The report leads with tidiness outcomes (paths removed, empty directories cleared, and the locked, changed, protected, needs-elevation, and unverified skips) and records logical / reclaimable bytes plus observed free-space delta as secondary figures.
The execution lane is Linux-only. It reads the current mount namespace from /proc/self/mountinfo,
re-discovers protections and Git state, opens every parent through O_NOFOLLOW directory descriptors,
checks the descriptor identities against the snapshot, and calls descriptor-relative unlink/rmdir.
Windows and macOS retain the complete audit/report lane but return execution-platform-unsupported
at preview. Backups remain the recovery boundary for user data.
- Python 3.11+ available on
PATHis required for scanning, validation, the skill-scoped guard, and cleanup. The floor's single origin is theMIN_PYTHONconstant inskills/clean/scripts/hygiene.py;/disk-hygiene:setup checkderives the enforced value from there, so treat the number printed here as a convenience copy. Guarded engine calls must use the same absolute interpreter reported by the skill-scoped guard, so Bash aliases and functions cannot replace it. The plugin never downloads a runtime. - Git is optional for ordinary trees. If a target contains or sits inside a Git worktree, Git becomes required so tracked content can be proven safe; otherwise cleanup for that subtree is blocked.
ghwith authenticated access to the configuredgithub.comremote is additionally required only when the operator invokes standalone-checkout VCS evidence mode. Other hosting providers remain protected; no generic network orgit ls-remotefallback is treated as provider proof.- Windows has the full audit lane (Python 3.11's
lstatreparse metadata plus Win32 APIs exposed by the OS; never invokes UAC) but engine execution is unsupported:previewreportsexecution-platform-unsupportedas a per-candidate blocker, and removal is a manual, per-path Recycle-Bin handoff offered only when--executewas requested and after explicit approval (the flag gates every deletion lane, manual included). The Recycle-Bin / Trash naming is a model-layer distinction only, the engine treats Windows and macOS identically (execution unsupported); which reversible-removal container the manual lane prefers is the model's instruction, not engine behavior. - Linux requires readable
/proc/self/mountinfo, descriptor-relative filesystem APIs, andlsoffor the optional execution lane. Absence, diagnostics, or authority gaps block cleanup. - macOS supports audit/report only because this implementation has no authoritative bind-mount and descriptor-anchoring proof for its execution lane.
Verify this machine's prerequisites and platform posture with /disk-hygiene:setup check;
/disk-hygiene:setup apply resolves anything the check reports with guidance.
All three hook registrations, both wired hooks and the skill-scoped belt, use shell form:
the command string names hooks/run-python-hook.sh directly with "shell": "bash" and no
args, so Claude Code routes them through Git Bash itself instead of resolving the command on
PATH. Exec form does not survive Windows, where a bare PATH lookup finds the WSL relay
System32\bash.exe before Git Bash, or the zero-length WindowsApps\python3.exe App Execution
Alias stub; the launch fails, and a failed hook launch is non-blocking, so the guard silently
enforces nothing. The launcher resolves Python itself instead (#1504).
The guard registers on two surfaces: a plugin-level engine gate (hooks/hooks.json) that acts
only on commands referencing the engine, deferring everything else instantly, and enforces the kill
switch and data-root authority; 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. Both surfaces resolve the kill switch by reading disk_hygiene_enabled from
user-scope pluginConfigs in settings.json (located from ${CLAUDE_PLUGIN_ROOT}, honored only
from user/managed/--settings scope since Claude Code 2.1.207, so a repo cannot forge it), register
unconditionally, and fail closed to enabled.
Hook-lifetime caveat: docs scope a skill hook to the component's lifetime, but session-long firing of the belt has been observed on at least one Claude Code build (producer-reported; see issue #1105). If unrelated commands are denied after a clean run ends, start a new session and see that issue. PreToolUse hooks also fire inside subagents, so fanned-out workers run under the same guards.
A silent engine-gate launch or runtime failure is surfaced. A Stop-event detector
(skills/clean/scripts/guard_launch_monitor.py, a separate hook entry in hooks/hooks.json,
independent of the engine-gate guard itself) scans the session transcript for
hook_non_blocking_error records naming the engine gate's own command string and warns once per
session with the failure count and the most recent failure's exit code, duration, and stderr, so a
guard that never ran or died mid-run does not look identical to a guard that ran and approved. This
covers only the destructive_guard.py command string in the current session's transcript: it does
not cover repo-hygiene's own guard (a separate plugin, verified working independently), and it never
retroactively scans a prior session's transcript. Every hook registration routes through
hooks/run-python-hook.sh, a bash launcher that resolves Python independently of bare python3 on
PATH, so when python3 is the WindowsApps alias stub or otherwise unresolvable, the detector still
emits a systemMessage even though the guard cannot run (#1504).
Windows python3 gotcha, the Store alias stub fails the guard open. Every hook resolves Python
through hooks/run-python-hook.sh (rejecting the zero-length WindowsApps\python3.exe App
Execution Alias stub and falling through to python, then py -3) before exec'ing the guard, the
skill-scoped belt included. When no interpreter resolves anywhere on the ladder, the guard fails
open: a PreToolUse hook blocks a tool call only by emitting exit code 2 or a deny decision
(Hooks); a guard that never runs emits neither, and Claude
Code treats the non-blocking result as approval, so the destructive Bash/PowerShell command proceeds
ungated. The Stop detector emits a systemMessage in that case so the blind spot is visible.
/disk-hygiene:setup check resolves the launcher's whole ladder and FAILs only when it is exhausted
or the interpreter it selects is below the floor. A stubbed python3 alongside a working python
or py -3 is a WARN, not a FAIL: every guard launches there, and the residual is only that a
bare python3 typed by hand still opens the Store. To clear it: disable the python3 App execution
alias (Settings > Apps > Advanced app settings > App execution aliases) or install real Python ahead
of WindowsApps on PATH. A bare command -v python3 / where python3 success is not proof the
interpreter is real, the stub answers to the name too.
Every verdict the guard reaches is appended to a local record under the plugin's own persistent data directory, with no configuration:
<CLAUDE_PLUGIN_DATA>/guard-decisions/decisions.jsonl
One JSON object per line, so tail, grep, and any JSON-aware reader all work with no
purpose-built tool:
{"schema_version":"1.0","timestamp":"2026-09-07T18:22:41.907Z","hook":"destructive-guard","decision":"ask","rule":"exact-engine-apply","tool":"Bash","mode":"engine-gate","command":"python3 <engine> apply --execute ...","reason":"disk-hygiene is ready to apply one exact, previewed tier..."}decision is one of allow, ask, deny, none (the guard ran and issued no
permissionDecision), or not-run. rule names the branch that fired, so a denial because
execution is switched off (kill-switch-disabled-apply) is distinguishable from a denial because
the command was not an exact engine invocation (not-exact-engine-command). The not-run records
come from the Stop detector, which is the only process that can observe a guard that never
launched.
- Bounded. The live file rotates to
decisions.previous.jsonlat 1 MiB, so the record holds at most about 2 MiB and never needs pruning.commandandreasonare secret-scrubbed, then clipped to 400 characters. - Command text is omitted on the catch-all arms. A PowerShell call recorded as
none(belt mode, no flagged spelling) and a Bash deny-by-default (not-exact-engine-command) persistcommand_chars(length only) instead of the command text. Those branches fire on arbitrary session commands. - Owner-only. The directory is created
0700and the live file0600. Mode is reapplied on every write so a leftover world-readable file is tightened. - Never a factor in a verdict. An unwritable data root, a full disk, or any other write failure records nothing and changes no decision: the verdict is computed and emitted before the record is attempted, and the write path raises nothing.
- Not a replacement for telemetry. A configured
HOOK_TELEMETRY_SINKkeeps receiving exactly what it received before. The local record is the floor beneath it, for the ordinary case where no sink exists. - What is not recorded. The plugin-level defer, the branch this hook takes for every Bash command that does not name the engine, writes nothing, which is what keeps the always-on path free. The watchdog's expiry path also writes nothing: that callback runs while the main thread is presumed wedged inside a filesystem call, and it stays syscall-free for exactly that reason.
- Turning it off. Set
DISK_HYGIENE_GUARD_DECISION_LOGto0,off,false, orno. Any other value, including an absent one, records.
/disk-hygiene:clean <target-directory>
/disk-hygiene:clean --policy <policy.json> <target-directory>
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.
Policy files all share one shape:
{
"version": 1,
"disabled_hint_ids": ["common-lock-file"],
"additional_hints": [
{
"id": "my-tool-staging",
"os": ["all"],
"kind": "name_glob",
"pattern": "my-tool-stage-*",
"confidence_ceiling": "medium",
"reason": "My tool's documented staging-directory convention"
}
],
"additional_protected_path_globs": ["client-deliverables/**"]
}Without --policy, standing policy files layer over the baseline when present:
~/.claude/disk-hygiene.json (user-global) first, then the consumer project's
.claude/disk-hygiene.json. An explicit --policy file is the invocation-specific choice and
replaces both standing layers. The scan output records which sources applied.
Candidate hints can be disabled or extended. Consumer protection globs are additive. Hard safety predicates and the baseline protected-name/root rules are non-overridable by any layer: a policy file can only add protections, add hints, or disable discovery hints (which can only cause junk to be missed, never removed).
When the audited zone overlaps the user temp directory, the scan also reports an os_autoclean
advisory naming the OS mechanism that should own it (Windows Storage Sense, systemd-tmpfiles) and,
when that mechanism is off or set to fire only on low disk space, recommends enabling it rather than
hand-cleaning the zone.
- Use
/repo-hygiene:cleanfor deterministic caches, build outputs, Git metadata, or a fresh-pull reset inside one repository.disk-hygienedoes not duplicate those mechanisms. - Use
/source-control:worktree status/cleanup(if installed) for git worktree checkouts such as a.worktrees/tree, run those actions from the checkout's own main repository, as they manage the current repository's worktrees and take no target.disk-hygieneprotects tracked content and.gitmetadata but does not manage worktree lifecycle. For a redundant standalone checkout, the manual handoff's optional VCS evidence mode can returnclearonly after all four proof gates in the safety model pass. - Use a product's own prune/GC/uninstall command for state it owns. This skill reports the handoff and records the native result but never makes managed state eligible for engine execution.
git cleanremains the authority for ignored/untracked repository files. This plugin protects every tracked path and does not emulate Git's path rules.
What this plugin can reach, what it refuses, and what it costs you to have it installed. The measurements below carry the conditions they were taken under.
- Code execution: the plugin runs bundled, standard-library Python. The skill-scoped PreToolUse
guard denies every unknown Bash command, permits only canonical bundled scan/preview calls, and
returns a hook-issued
askfor the canonical engine apply call (samepermissionDecision: "ask"as the PowerShell deletion lane;dontAskauto-denies instead of prompting). The guard rejects shell expansion and operator syntax instead of validating only the post-split argument vector; script identity follows the host path rules and remains case-sensitive on POSIX. Noeval, dynamic shell construction, or downloads are used. Paths cross the process boundary as JSON or individually quoted CLI arguments. - MCP / external trust: no MCP server, agent, dependency, or third-party service is shipped.
- Configuration: one non-sensitive
userConfigboolean (disk_hygiene_enabled, defaulttrue) gating the execution tiers. Setting itfalseputs/disk-hygiene:cleanin audit-only mode. Both guard surfaces resolve the toggle by readingdisk_hygiene_enabledfrom user-scopepluginConfigsinsettings.json(not the process environment). A configuredfalsedenies Bash engine invocations outright on the always-on engine gate (whether or not the clean skill is active); PowerShell deletion spellings are denied outright by the skill-scoped belt while/disk-hygiene:cleanis active (the always-on gate defers on non-engine commands). The read is honored only from user, managed, and--settingsscope (Claude Code 2.1.207+), so a project or local reposettings.jsoncannot flip it; the user file is located from${CLAUDE_PLUGIN_ROOT}, not from repo-redirectable environment, and the managed (enterprise) file at its fixed system path wins as the highest-precedence scope so an org can enforce audit-only (the siblingmanaged-settings.d/drop-in directory is merged over it). An absent or unreadable value fails closed to enabled. The one residual a hook cannot read is a value supplied only via a session--settingsfile. The skill's own kill-switch probe + skill-content value remain a defense-in-depth honoring layer over the guard. - Trust-surface record (0.7.0; updated 0.17.8): the plugin-level
hooks/hooks.jsonPreToolUse 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: a fixed launch string authored in the plugin's ownhooks.json(see the 0.17.8 delta for exactly what that bounds now that the string reaches a shell), bundled standard-library scripts 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, with the always-on share measured per the hook-budget convention's method (EPOCHREALTIMEwall-clock around direct hook invocation with a benign representative payload; Windows 11 + Git Bash dev host, 2026-08-16): the engine-gate hook costs ≈ 190–300 ms per Bash/PowerShell tool call across batches (92 single runs). ≈ 19–30% of the convention's ≤ 1 s typical per-tool-call ceiling. While thecleanskill is loaded, its frontmatter registration is a second matching hook that the harness launches in parallel; the pair measured concurrently (&+wait, 60 pairs) walls at ≈ 320–410 ms, ≈ 1.3–1.5× the same-batch single-hook wall rather than double it. Superseded in 0.21.0 for the launcher portion: thesedread of the engine (≈ 24 ms, ≈ 13% of the hook's cost, and ≈ 38 ms in the two-full-pass form before 0.20.13) no longer exists, and neither does the separate Python process that was spawned only to evaluate the version predicate. The floor is now recovered inside the candidate interpreter on the cold path, and the resolved interpreter is cached, so a warm invocation spends one process spawn (the guard itself) where it previously spent four:dirname,sed, and twopython3. That spawn census, not a duration, is the durable figure: it is deterministic, whereas the wall-clock share above was measured on a host whose process-creation cost was later observed varying more than tenfold within a single hour under contention (bash -c trueat 283 ms and 1825 ms in the same session at ~10% CPU). Interleaved before/after on such a host, 24 alternating pairs, measured p50 5446 → 1418 ms and p95 16991 → 7874 ms; those absolute values are specific to that contention and are not comparable to the ≈ 190–300 ms figures above, which were taken on a quiet host. Re-measure per the convention's method on a quiet host before citing a new share. Superseded in 0.23.1 for every shell call that does not name the engine and does not invoke it through a variable. The gate is registered once per tool. Bash carriesBash(*hygiene.py*). PowerShell carriesPowerShell(*hygiene.py*)plusPowerShell(*python*$*)andPowerShell(*& $*), because the PowerShell matcher evaluates collected command nodes and a literal path in$script = '.../hygiene.py'is not part of the laterpython $script(or& $script) command. Aniffilter is scoped to the tool it names, and oneBash(...)filter under aBash|PowerShellmatcher left every PowerShell call unguarded. The harness evaluates the filter through the tool's own permission matcher before it spawns anything, so a Bash or PowerShell call that does not name the engine and does not invoke an interpreter or call-operator through a variable now costs this plugin zero processes and zeroexecvecalls. Before, on a warm interpreter cache, every PowerShell call paid fourexecvecalls (bash -c, the launcher through itsenvshebang, bash, the interpreter), no fork, and a 106 KB module import, to be told it was irrelevant; measured withstrace -fon Linux, where the hook process walled at p50 44 ms against abash -c :floor of 2 ms (n = 20 per tool), about 22 spawn-equivalents, the cost class the issue measured as a 2.4 s median on Windows. A call that names the engine, or invokes python/&through a variable, pays that chain unchanged and is judged unchanged. What the filters still cannot see: for Bash, a command containing$(), a backtick or$VARspawns the guard anyway, because the filter cannot see what the substitution expands to; for PowerShell, the matcher parses the command and runs the hook when any statement, pipeline element or nested command matches, so a mixed line such asGet-Date; python hygiene.pystill reaches the guard (the every-subcommand rule applies to allow decisions, not toif). Neither filter sees an engine reached without its own file name in a command node and without an interpreter or call-operator variable, any spelling such as a symlink or hard link under another name or a Win32 8.3 short name; the gate's relevance check could catch that case by file identity, and the residual is accepted on both lanes, as it has been on the Bash lane since 0.21.4, because the engine's own preview and approval-token containment still answers for it. 0.23.0 delta (local decision record): the guard now appends one line to<CLAUDE_PLUGIN_DATA>/guard-decisions/decisions.jsonlon every branch that reaches a verdict. The added trust surface is that one append, to a path under the plugin's own data root and nowhere else, no read of anything new and no process. The always-on defer branch, which is what a Bash command that does not name the engine takes, writes nothing and is byte-for-byte the path it was. Measured withstrace -f -e trace=clone,clone3,fork,vfork,execve,openat,writeon a Linux container againstorigin/mainat6db96637d, five invocations per arm: the process and exec census is unchanged on both paths, oneexecve(the guard) and oneclone3(the watchdog thread,CLONE_THREAD, not a process), before and after. The record itself costs oneopenatplus onewriteplus onechmod(the live file,0600) on a warm data root, and one extra failedopenatplus onemkdirplus onechmod(the directory,0700) on the first write of an install. Wall-clock over 40 invocations per arm, alternated twice, moved inside run-to-run noise on that host (52–58 ms both before and after, the sign of the difference changing between repetitions), which is why the syscall census rather than a duration is the figure cited here. On a machine where no Python 3 interpreter resolves at all the gate fails open on every call, theStopdetector emits asystemMessagefor that case, so the blind spot is visible rather than silent (#1110, #1504). 0.9.0 delta: the gate no longer carries a${user_config.*}argument (which, unset, dropped the whole hook and left the gate inert on a default install); it now registers unconditionally and resolves the kill switch by reading the usersettings.jsonand the platform managed-settings.json. The added trust surface is that settings-file read, bounded to a singlepluginConfigsvalue, from the user file (located from${CLAUDE_PLUGIN_ROOT}) and the root-owned managed file at its fixed system path, no write. Both are the plugin's own documented CC config, sanctioned by the acceptance review's operator-home carve-out (criterion 4). This entry is the plugin-acceptance review delta for the change. 0.17.8 delta (launch form): both wired hooks now register in shell form. Thecommandstring nameshooks/run-python-hook.shwith"shell": "bash"and noargs, because exec form's barePATHlookup forbashresolved to the WSL relay on Windows and the guard silently never launched (#1416). Stated plainly: a shell now parses the launch string, so "no shell involved" is no longer what bounds this surface. What bounds it instead is that the string is a fixed literal in the plugin's ownhooks.jsonwith no model-, repo-, or session-supplied text interpolated into it; the only values substituted are Claude Code's own${CLAUDE_PLUGIN_ROOT}and${CLAUDE_PLUGIN_DATA}, and each is double-quoted, so the shell's re-tokenization reproduces the exec-form argument vector byte-for-byte, verified for both hooks against roots containing spaces and backslashes. The limits of that quoting belong in the record too: Claude Code substitutes those placeholders textually before bash parses the result, so the double quotes bound whitespace and backslashes but would not neutralize a$or a backtick inside a substituted value (both placeholders resolve under Claude Code's own install and data roots). The invariant is therefore maintained by test, not structural,hooks/run-python-hook.test.shasserts the launcher is named incommand,argsis absent,shell: bashis declared, and every placeholder is double-quoted, andtest_hygiene.py's hook helpers are form-agnostic so a shell-form entry can never make an assertion vacuously green. Interpolating anything beyond those two placeholders into the command string would open a live injection surface; a repo-wide CI gate for this defect class is proposed in #2569. 0.17.9 delta (launch form, skill surface): the skill-scoped belt inskills/clean/SKILL.mdfrontmatter moves to the same shell form, for the same reason one rung down, its exec-formcommandwas the literalpython3, which on stock Windows is the zero-lengthWindowsAppsApp Execution Alias stub, so the belt could not launch there at all (#2568). The trust analysis above carries over with a narrower substitution set: a skill-frontmatter hook receives only${CLAUDE_PLUGIN_ROOT}(#1014), never${CLAUDE_PLUGIN_DATA}or${user_config.*}, so the belt's command string carries exactly one placeholder and the--authorized-data-rootchannel stays out of it by construction. Because this belt was working whereverpython3resolved to a real interpreter, the conversion was held to argv equivalence: the vectordestructive_guard.pyreceives is byte-identical before and after, asserted against roots containing spaces and backslashes; only argv[0] changes, from an interpreter name to the launcher path. What this does not change is the guard's no-interpreter behaviour, the launcher still exits 0 silently in guard mode when nothing on the ladder resolves. A directhygiene.pyinvocation 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 for the degraded-mode detail). The engine never reads or stores credentials; standalone-checkout evidence delegates one exact commit lookup per local head to the already-authenticatedghCLI. Policy comes from an explicit invocation argument or standingdisk-hygiene.jsonfiles under~/.claude/and the consumer project's.claude/. All policy input is pattern-only and additive: it can add protections and discovery hints or disable hints, and cannot weaken hard guards or authorize removal, so ambient config cannot widen the destructive surface. - Isolation: bundled assets resolve from
${CLAUDE_PLUGIN_ROOT}; generated state belongs under${CLAUDE_PLUGIN_DATA}. The audited target is read, then mutated only through the gated lane. - Egress: none in the ordinary audit/preview/apply paths. Opt-in standalone-checkout evidence
invokes
gh apiagainstgithub.comonly, using owner/repository coordinates parsed from the checkout's configured remote and an exact locally observed SHA. Unsupported hosts fail closed.gitandlsofremain local read-only subprocesses. - Provenance: Melodic Software, MIT. No vendored code.
Security review result: accept for the declared local code-execution surface. Any later network, credential, dependency, or MCP surface reopens this review.
Verified 2026-07-16 against current primary documentation:
- Create plugins and
plugins reference. Plugin structure, cache
isolation, manifests, versions, and local
--plugin-dirtesting. - Skills. Side-effecting skills should be manual-only; supporting files, arguments, and skill-scoped hooks.
- Hooks. Current
PreToolUsedecision output. - Create a marketplace. Relative plugin sources.
- GNU Bash shell expansions . Expansion order and the brace, tilde, parameter, command, arithmetic, process, splitting, and filename-expansion families rejected by the literal-command guard.
- Python 3.11 filesystem APIs and path APIs. Non-following metadata, junction, and mount detection.
- Windows reparse-point operations
and
GetLogicalDrives: the reparse attribute and available-volume enumeration. - Linux
mountinfo. Current mount namespace and bind-mount targets, whichos.path.ismountcannot reliably identify. - Git
ls-files. The index/tracked-file authority. - Windows
CreateFile: sharing conflicts and directory handles viaFILE_FLAG_BACKUP_SEMANTICS. lsofmaintained documentation. Open-file lookup; the recursive+Dauthority limitation is why diagnostics fail closed.- POSIX
unlinkand Linuxunlink(2). Open-file unlink semantics motivate an explicit preflight rather than relying on deletion failure.
Generated from this plugin's .claude-plugin/plugin.json. Every option Claude Code
will prompt for when the plugin is enabled, with the environment variable each hook
reads it from.
| Option | Type | Default | Environment variable | Description |
|---|---|---|---|---|
disk_hygiene_enabled |
boolean | true |
CLAUDE_PLUGIN_OPTION_DISK_HYGIENE_ENABLED |
Allow the clean skill's execution tiers; false = audit-only mode |
Three supported routes, in the order most people want them:
-
Interactively. Claude Code prompts for declared options when you enable the plugin. To change them later:
/plugin configure disk-hygiene@<marketplace>. -
Headless. Repeat
--configfor each option. Replace<marketplace>with the marketplace you installed this plugin from:claude plugin install disk-hygiene@<marketplace> -s <scope> --config disk_hygiene_enabled=<value>
The same command reconfigures a plugin that is already installed: it prints
already installedand still writes the value. The short-circuit message is about the install, not the config write. Do notclaude plugin uninstallto reconfigure: uninstalling drops this plugin's whole storedpluginConfigsentry, resetting every option in the table above to its default.-sdefaults touser, so pass the scopeclaude plugin listreports for this plugin. The verified-version record lives in the plugin-reconfiguration convention.The value is stored immediately; the session you are in does not change. Hooks are handed their
CLAUDE_PLUGIN_OPTION_*when the session starts, so start a fresh Claude Code session before expecting new behavior. A check run in the old session still reports the old value, and that is not a failed write. -
By hand, in settings. Add the value under
pluginConfigsin your user settings (~/.claude/settings.json):{ "pluginConfigs": { "disk-hygiene@<marketplace>": { "options": { "disk_hygiene_enabled": <value> } } } }Plugin option values are read from user,
--settings, and managed settings only, not from a project's.claude/settings.json. To vary behavior per repository, enable or disable the plugin in that project'senabledPluginsinstead of setting an option there.
Do not set the CLAUDE_PLUGIN_OPTION_* variables yourself. They are how Claude Code
hands a configured value to a hook process; the value comes from the routes above.
- User configuration: the
userConfigschema and theCLAUDE_PLUGIN_OPTION_<KEY>export - Plugin install options: the
--configflag's reference entry - Plugins and skills settings:
enabledPlugins,extraKnownMarketplaces,pluginConfigs - Settings files and who they affect: user vs project vs local precedence
- Manage installed plugins: enabling, disabling,
/plugin list
MIT (SPDX-License-Identifier: MIT). See the repository root LICENSE.