Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

disk-hygiene

/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.

Safety contract

  • 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-children with 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 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.
  • 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 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-relative rmdir. 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.

Requirements and platform support

  • Python 3.11+ available on PATH is required for scanning, validation, the skill-scoped guard, and cleanup. The floor's single origin is the MIN_PYTHON constant in skills/clean/scripts/hygiene.py; /disk-hygiene:setup check derives 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.
  • gh with authenticated access to the configured github.com remote is additionally required only when the operator invokes standalone-checkout VCS evidence mode. Other hosting providers remain protected; no generic network or git ls-remote fallback is treated as provider proof.
  • Windows has the full audit lane (Python 3.11's lstat reparse metadata plus Win32 APIs exposed by the OS; never invokes UAC) but engine execution is unsupported: preview reports execution-platform-unsupported as a per-candidate blocker, and removal is a manual, per-path Recycle-Bin handoff offered only when --execute was 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, and lsof for 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.

How the guard is registered

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.

Reading guard decisions after the fact

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.jsonl at 1 MiB, so the record holds at most about 2 MiB and never needs pruning. command and reason are 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) persist command_chars (length only) instead of the command text. Those branches fire on arbitrary session commands.
  • Owner-only. The directory is created 0700 and the live file 0600. 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_SINK keeps 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_LOG to 0, off, false, or no. Any other value, including an absent one, records.

Usage

/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.

Relationship to other tools

  • Use /repo-hygiene:clean for deterministic caches, build outputs, Git metadata, or a fresh-pull reset inside one repository. disk-hygiene does 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-hygiene protects tracked content and .git metadata but does not manage worktree lifecycle. For a redundant standalone checkout, the manual handoff's optional VCS evidence mode can return clear only 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 clean remains the authority for ignored/untracked repository files. This plugin protects every tracked path and does not emulate Git's path rules.

Security posture

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 ask for the canonical engine apply call (same permissionDecision: "ask" as the PowerShell deletion lane; dontAsk auto-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. No eval, 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 userConfig boolean (disk_hygiene_enabled, default true) gating the execution tiers. Setting it false puts /disk-hygiene:clean in audit-only mode. Both guard surfaces resolve the toggle by reading disk_hygiene_enabled from user-scope pluginConfigs in settings.json (not the process environment). A configured false denies 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:clean is active (the always-on gate defers on non-engine commands). The read is honored only from user, managed, and --settings scope (Claude Code 2.1.207+), so a project or local repo settings.json cannot 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 sibling managed-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 --settings file. 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.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: a fixed launch string authored in the plugin's own hooks.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 (EPOCHREALTIME wall-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 the clean skill 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: the sed read 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 two python3. 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 true at 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 carries Bash(*hygiene.py*). PowerShell carries PowerShell(*hygiene.py*) plus PowerShell(*python*$*) and PowerShell(*& $*), because the PowerShell matcher evaluates collected command nodes and a literal path in $script = '.../hygiene.py' is not part of the later python $script (or & $script) command. An if filter is scoped to the tool it names, and one Bash(...) filter under a Bash|PowerShell matcher 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 zero execve calls. Before, on a warm interpreter cache, every PowerShell call paid four execve calls (bash -c, the launcher through its env shebang, bash, the interpreter), no fork, and a 106 KB module import, to be told it was irrelevant; measured with strace -f on Linux, where the hook process walled at p50 44 ms against a bash -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 $VAR spawns 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 as Get-Date; python hygiene.py still reaches the guard (the every-subcommand rule applies to allow decisions, not to if). 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.jsonl on 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 with strace -f -e trace=clone,clone3,fork,vfork,execve,openat,write on a Linux container against origin/main at 6db96637d, five invocations per arm: the process and exec census is unchanged on both paths, one execve (the guard) and one clone3 (the watchdog thread, CLONE_THREAD, not a process), before and after. The record itself costs one openat plus one write plus one chmod (the live file, 0600) on a warm data root, and one extra failed openat plus one mkdir plus one chmod (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, the Stop detector emits a systemMessage for 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 user settings.json and the platform managed-settings.json. The added trust surface is that settings-file read, bounded to a single pluginConfigs value, 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. The command string names hooks/run-python-hook.sh with "shell": "bash" and no args, because exec form's bare PATH lookup for bash resolved 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 own hooks.json with 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.sh asserts the launcher is named in command, args is absent, shell: bash is declared, and every placeholder is double-quoted, and test_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 in skills/clean/SKILL.md frontmatter moves to the same shell form, for the same reason one rung down, its exec-form command was the literal python3, which on stock Windows is the zero-length WindowsApps App 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-root channel stays out of it by construction. Because this belt was working wherever python3 resolved to a real interpreter, the conversion was held to argv equivalence: the vector destructive_guard.py receives 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 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 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-authenticated gh CLI. Policy comes from an explicit invocation argument or standing disk-hygiene.json files 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 api against github.com only, using owner/repository coordinates parsed from the checkout's configured remote and an exact locally observed SHA. Unsupported hosts fail closed. git and lsof remain 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.

Sources

Verified 2026-07-16 against current primary documentation:

Configuration

Options reference

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

How to set these

Three supported routes, in the order most people want them:

  1. Interactively. Claude Code prompts for declared options when you enable the plugin. To change them later: /plugin configure disk-hygiene@<marketplace>.

  2. Headless. Repeat --config for 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 installed and still writes the value. The short-circuit message is about the install, not the config write. Do not claude plugin uninstall to reconfigure: uninstalling drops this plugin's whole stored pluginConfigs entry, resetting every option in the table above to its default. -s defaults to user, so pass the scope claude plugin list reports 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.

  3. By hand, in settings. Add the value under pluginConfigs in 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's enabledPlugins instead 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.

Upstream documentation

License

MIT (SPDX-License-Identifier: MIT). See the repository root LICENSE.