diff --git a/docs/conventions/hook-budget/README.md b/docs/conventions/hook-budget/README.md index 3086c6e33c..1dd14f5ad7 100644 --- a/docs/conventions/hook-budget/README.md +++ b/docs/conventions/hook-budget/README.md @@ -109,10 +109,11 @@ recorded in the hook-performance program's DEVIATIONS log. ## Exec-form fleet sweep -Every shipped hook row is exec form ([#3686](https://github.com/melodic-software/claude-code-plugins/issues/3686)) with `"command": "node"`. A row whose script is bash runs `hooks/exec-bash.mjs` (canonical copy [`lib/exec-bash.mjs`](../../../lib/exec-bash.mjs)) and then the script; a row whose script is Node names that script directly. The bullets state what the sweep costs and what it needs. +Every shipped hook row, except the shell-form rows named under "Scope", is exec form ([#3686](https://github.com/melodic-software/claude-code-plugins/issues/3686)) with `"command": "node"`. A row whose script is bash runs `hooks/exec-bash.mjs` (canonical copy [`lib/exec-bash.mjs`](../../../lib/exec-bash.mjs)) and then the script; a row whose script is Node names that script directly. The bullets state what the sweep costs and what it needs. - **What shipped.** Every row in `plugins/*/hooks/hooks.json` and in the skill-frontmatter hooks of - `disk-hygiene:clean` and `repo-hygiene:clean` carries `args` and `"command": "node"`. Option gates + `disk-hygiene:clean` and `repo-hygiene:clean`, except the shell-form rows named under "Scope", + carries `args` and `"command": "node"`. Option gates that used to be shell tests are launcher flags (`--require-true`, `--run-if-unset-or-true`). Two scripts check the spelling. `scripts/check-exec-form-windows-probe.sh` rejects a `.sh` path, a `.cmd`/`.bat` shim, or bare `bash` as `command`; a non-Windows skip of its spawn half does not show @@ -124,15 +125,16 @@ Every shipped hook row is exec form ([#3686](https://github.com/melodic-software Node script named in `args`. A bash-scripted row behind the launcher is k = 2 (node, then bash), plus 1 per program the script starts. That is the shell-form line's "a script is at least 2", so the sweep is not a spawn saving. -- **Prerequisite.** Node on PATH is now required for every hook (`command` is `node`). The launcher +- **Prerequisite.** Node on PATH is now required for every exec-form hook (`command` is `node`). The launcher cannot detect a missing node, because the launcher is a node process; a failed launch is non-blocking, so the guard then enforces nothing (the [philosophy Hooks row](../../plugin-philosophy.md#component-stances)). With node present and bash unresolvable, the launcher exits 1: a non-blocking hook error and not a guard block, and the guard script does not run (the header of [`lib/exec-bash.mjs`](../../../lib/exec-bash.mjs)). -- **Scope.** No shell-form row remains in the shipped hook surfaces named under "What shipped". - Neither check script inspects a shell-form row, so no gate enforces that absence. The philosophy +- **Scope.** Three rows stay shell form so they can report a missing `node`: the `SessionStart` notice rows in `guardrails` and `disk-hygiene`, and the `hook-failure-audit` Stop row in `claude-ops`. + Neither check script inspects a shell-form row; each of the three plugins' own hook tests pins its + row's shell form, so a sweep back to `node` fails that test. The philosophy Hooks row makes exec form mandatory only where `${user_config.*}` appears, so exec form fleet-wide is this sweep's choice. - **Measurement.** The reference figures above (Windows, 2026-07-31 and 2026-09-02) were taken before diff --git a/docs/plugin-philosophy.md b/docs/plugin-philosophy.md index 1147c9742a..8f4be8f9e5 100644 --- a/docs/plugin-philosophy.md +++ b/docs/plugin-philosophy.md @@ -307,7 +307,7 @@ re-deriving a row. | [`commands/`](https://code.claude.com/docs/en/plugins-reference) | Prohibited | Officially merged into skills; docs direct "use `skills/` for new plugins". Existing flat commands migrate to skill directories. | 2026-07-17 | | [Agents](https://code.claude.com/docs/en/sub-agents) | Adopt on need | Plugin agents do not support `hooks`, `mcpServers`, or `permissionMode` (security restriction). Design within that limit rather than working around it. | 2026-07-17 | | [Workflows](https://code.claude.com/docs/en/workflows) | Adopt on need | Native and not experimental: a script in `workflows/`, or wherever the `workflows` manifest field points (that field replaces the default scan), runs as a plugin-namespaced `/plugin:name` command. Availability, not maturity, is the constraint: workflows are paid-plan-gated, a consumer can switch them off (`disableWorkflows`, `CLAUDE_CODE_DISABLE_WORKFLOWS`), and an org can disable them fleet-wide in managed settings; so, as with `bin/`, never make a workflow the only path to a capability. Not "Wait": the [deferred workflow engines](adr/0020-defer-three-medley-surfaces-with-explicit-recheck-triggers.md) are a named candidate carrying a live trigger, so the gap is identified rather than hypothetical. None ship in this fleet today. | 2026-07-27 | -| [Hooks](https://code.claude.com/docs/en/hooks) | Adopt on need | Exec form (`args`) is mandatory wherever `${user_config.*}` appears, because shell form errors since v2.1.207; otherwise read the `CLAUDE_PLUGIN_OPTION_` mirror. Windows exec form spawns a real executable such as a `.exe` with the `args` array and no shell, so a shebang script or a `.cmd`/`.bat` shim is not a `command`, and neither is a bare `bash`, `sh`, `python`, or `python3` (a failed launch is non-blocking, so a guard then enforces nothing). Shell form with `"shell": "bash"` stays legal where no `${user_config.*}` appears; every plugin hook row uses exec form, `"command": "node"` with the script path in `args`. `node` must be on `PATH`, and Claude Code does not guarantee it: exec form resolves `command` on `PATH` ([Exec form and shell form](https://code.claude.com/docs/en/hooks#exec-form-and-shell-form)), and the installed `claude` binary does not itself invoke Node ([Install with npm](https://code.claude.com/docs/en/setup#install-with-npm)), both fetched 2026-09-29. A hook that cannot start is a non-blocking error, so a guard whose `node` is missing enforces nothing and the transcript notice is the only signal ([Other exit codes](https://code.claude.com/docs/en/hooks#other-exit-codes)). `scripts/check-hook-exec-form.sh` rejects a bare name other than `node`. `scripts/check-exec-form-windows-probe.sh` rejects a script path used as `command`; its non-Windows skip does not authorize converting `.sh` rows. The four-part record is [Windows exec-form probe](#windows-exec-form-probe). Hooks modules ("mods"), the in-process TypeScript hook form, are deferred: see the mods row under [Recorded gate runs](#recorded-gate-runs) and [ADR 0035](adr/0035-defer-claude-code-mods-with-five-go-criteria.md). | 2026-09-29 | +| [Hooks](https://code.claude.com/docs/en/hooks) | Adopt on need | Exec form (`args`) is mandatory wherever `${user_config.*}` appears, because shell form errors since v2.1.207; otherwise read the `CLAUDE_PLUGIN_OPTION_` mirror. Windows exec form spawns a real executable such as a `.exe` with the `args` array and no shell, so a shebang script or a `.cmd`/`.bat` shim is not a `command`, and neither is a bare `bash`, `sh`, `python`, or `python3` (a failed launch is non-blocking, so a guard then enforces nothing). Shell form with `"shell": "bash"` stays legal where no `${user_config.*}` appears; every plugin hook row uses exec form, `"command": "node"` with the script path in `args`, except the guardrails and disk-hygiene SessionStart node notice rows and the claude-ops hook-failure-audit Stop row, which run in shell form with `"shell": "bash"` because they must work when `node` is missing. `node` must be on `PATH`, and Claude Code does not guarantee it: exec form resolves `command` on `PATH` ([Exec form and shell form](https://code.claude.com/docs/en/hooks#exec-form-and-shell-form)), and the installed `claude` binary does not itself invoke Node ([Install with npm](https://code.claude.com/docs/en/setup#install-with-npm)), both fetched 2026-09-29. A hook that cannot start is a non-blocking error, so a guard whose `node` is missing enforces nothing and the transcript notice is the only signal ([Other exit codes](https://code.claude.com/docs/en/hooks#other-exit-codes)). `scripts/check-hook-exec-form.sh` rejects a bare name other than `node`. `scripts/check-exec-form-windows-probe.sh` rejects a script path used as `command`; its non-Windows skip does not authorize converting `.sh` rows. The four-part record is [Windows exec-form probe](#windows-exec-form-probe). Hooks modules ("mods"), the in-process TypeScript hook form, are deferred: see the mods row under [Recorded gate runs](#recorded-gate-runs) and [ADR 0035](adr/0035-defer-claude-code-mods-with-five-go-criteria.md). | 2026-09-29 | | [MCP servers](https://code.claude.com/docs/en/mcp) | Adopt on need | Clears the plugin-acceptance security review for egress and trust delegation. Also the only component type that can cost a consumer their prompt cache: every other kind only appends to the request, while enabling or disabling a plugin that provides an MCP server forces a full re-read whenever the server's tools load into the prefix instead of being deferred by tool search ([actions that invalidate the cache](https://code.claude.com/docs/en/prompt-caching#actions-that-invalidate-the-cache), verified 2026-08-10). | 2026-08-10 | | [LSP servers](https://code.claude.com/docs/en/plugins-reference) | Adopt on need | Consumer must have the language-server binary; declare the prerequisite per the failure-behavior rules. | 2026-07-17 | | [Output styles](https://code.claude.com/docs/en/plugins-reference) | Adopt on need | No additional constraints. | 2026-07-17 | @@ -320,7 +320,7 @@ re-deriving a row. ### Windows exec-form probe -`scripts/check-exec-form-windows-probe.sh` rejects an exec-form `command` that is not a real Windows executable ([#3686](https://github.com/melodic-software/claude-code-plugins/issues/3686)). It does not rewrite rows. A `.sh` path, a `.cmd`/`.bat` shim, or bare `bash` as `command` stays illegal. `scripts/check-hook-exec-form.sh` keeps rejecting bare `bash` with the script in `args`. Every shipped hook row is exec form: `"command": "node"` with `hooks/exec-bash.mjs` (canonical `lib/exec-bash.mjs`, copied by `scripts/sync-exec-bash.sh`) and then the script. The launcher finds Git Bash and never `System32\bash.exe`. A default-off option is `--require-true NAME` (exit 0 unless `CLAUDE_PLUGIN_OPTION_NAME` is `true`). A default-on option is `--run-if-unset-or-true NAME` (exit 0 only when that variable is set to something other than `true`). Skill-frontmatter `args` is a YAML sequence, one element per argument. No shell-form hook row remains. +`scripts/check-exec-form-windows-probe.sh` rejects an exec-form `command` that is not a real Windows executable ([#3686](https://github.com/melodic-software/claude-code-plugins/issues/3686)). It does not rewrite rows. A `.sh` path, a `.cmd`/`.bat` shim, or bare `bash` as `command` stays illegal. `scripts/check-hook-exec-form.sh` keeps rejecting bare `bash` with the script in `args`. Every shipped hook row is exec form, except the three shell-form rows named in the Hooks row above: `"command": "node"` with `hooks/exec-bash.mjs` (canonical `lib/exec-bash.mjs`, copied by `scripts/sync-exec-bash.sh`) and then the script. The launcher finds Git Bash and never `System32\bash.exe`. A default-off option is `--require-true NAME` (exit 0 unless `CLAUDE_PLUGIN_OPTION_NAME` is `true`). A default-on option is `--run-if-unset-or-true NAME` (exit 0 only when that variable is set to something other than `true`). Skill-frontmatter `args` is a YAML sequence, one element per argument. - **Claim:** On Windows, exec form (`args` present) resolves `command` as an executable and spawns it directly with `args` as the argument vector. There is no shell, so a shebang is not honored, and `command` must be a real executable such as a `.exe`. `.cmd` and `.bat` shims cannot be spawned. If a Windows spawn of that shape drops `args` or the process image is `bash.exe`, the fleet sweep stops. - **Basis:** [Hooks reference](https://code.claude.com/docs/en/hooks), section "Exec form and shell form". Verbatim, from a full raw-markdown read of `https://code.claude.com/docs/en/hooks.md` (330,813 bytes, SHA-256 `57e3b47d55acfbae3dcdc112866c8c0f75528d8b5c4fca9bfcdaa904d4728218`; the slug is listed in `https://code.claude.com/docs/llms.txt`): "On Windows, exec form requires `command` to resolve to a real executable such as a `.exe`." The same section states that exec form has no shell and that `shell` is "Ignored when `args` is set". Args-drop is [anthropics/claude-code#90495](https://github.com/anthropics/claude-code/issues/90495), open as of this date. diff --git a/plugins/claude-ops/.claude-plugin/plugin.json b/plugins/claude-ops/.claude-plugin/plugin.json index b07946af39..0534506071 100644 --- a/plugins/claude-ops/.claude-plugin/plugin.json +++ b/plugins/claude-ops/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "claude-ops", - "version": "0.77.0", + "version": "0.77.1", "description": "Claude Code operations toolkit. Thirteen skills: audit-skill-visibility (audit whether each installed skill is actually VISIBLE to the model, and diagnose why most of a fleet never gets used: a skill is invisible when its description is dropped by Claude Code's skill-listing context budget, which sheds descriptions lowest-score-first so an unused skill loses the keywords that would let it be matched, from skills genuinely not wanted, from skills the run cannot observe at all; computes whether the listing overflows from documented settings, and withholds every cold verdict the data cannot support rather than reporting absence of data as absence of use), inventory (read-only enumeration of the complete invocable surface: every built-in CLI command with aliases and hidden/gated status, every bundled skill, every built-in subagent and tool, and every component of every installed plugin across all marketplaces; reads the shipped binary because upstream publishes no built-in command list, and carries an integrity verdict so a drifted build reports counts as floors rather than silently short totals), audit-install-state (read-only audit of the machine-scope ~/.claude installation directory and ~/.claude.json: full inventory split into an authored surface and rolled-up bulk trees, product-managed retention vs genuinely unmanaged state, filename-scheme resolution before any process-liveness check, and deliberate/mid-experiment detection; reports, never deletes), audit-performance (read-only slowness-diagnostic capture run at the moment the machine or a session feels slow: CLI version, retention-sweep health including the unparsable-settings pause, which warns in /status, a timed census walk of the install tree as a sweep-cost proxy, active-session and plugin-fleet counts, a process census, and the fan-out layer, which covers a load-labeled no-op spawn baseline, every hook that will fire bucketed per-tool-call versus per-turn with its invocation shape, the configured statusline, subagent concurrency and spawn-depth ceilings against documented defaults, whether running sessions predate the settings file they are judged by, and orphan attribution by parent liveness rather than age, plus on Windows a kernel-object census (Token objects against uptime, paged pool) that names a host-level leak beneath all four suspects; read against a bundled known-performance-issues reference that also records the causes tested and cleared; separates the four documented suspects of accumulated state, version regression, component bloat, and per-spawn fan-out cost, and routes remediation out; reports, never mutates, and never executes a discovered hook or statusline command), audit-native-overlap (map native Claude Code surfaces, namely built-in CLI commands, bundled skills, plugin-backed built-ins, and session-provided skills, against the current repo's plugin skills and agents, so a custom component never silently duplicates what Claude Code itself ships; bare invocation is a read-only overlap report carrying the extraction's integrity floors and a shared-listing-budget exposure section, verdicts are human-gated in a committed store rendered into a generated registry whose every row carries an observable recheck trigger, and only an explicit apply step bakes presence-gated native references into descriptions and Boundary sections), observability (read locally captured telemetry from the OTEL store, the collector, the per-session hook event log and hook-event JSONL, and ccusage, with trend reports, a per-session report of what fired, what was blocked and the event timeline, and store pruning), known-issues (search known Claude product GitHub bugs, check service health, maintain a persistent tracked-issue registry), changelog (ingest Claude Code changelog entries and turn them into decisions: apply executes those in scope one PR per owner plugin and hands larger ones off as work items, then re-extract the native surface and file its drift as work items), prerequisites (read-only table of external binaries declared by enabled plugins; never installs), plugins (bring a machine's plugin fleet current on demand: marketplace refresh, effective-scope updates including in-repo project/local installs, new-plugin install per policy, scope-divergence detection and explicit convergence), morning-brief (read-only gh-based operator morning view: queue-label counts, merge-ready PRs, parked decisions with their RECOMMENDED lines, and loop-lane telemetry freshness), lanes (start/restart/stop/status loop lanes as named background Claude Code sessions seeded from canonical prompt files, with per-lane model/effort, a repo-pull + marketplace-refresh launch step, and a consume-restarts action, an OS-schedulable reader that relaunches stopped lanes whose telemetry carries a restart_request), and a re-runnable setup action that settles where the known-issues registry, the skill-usage log and the hook log root live, places the root's self-ignoring guard, and detects retired conventions. Plus an opt-in, default-off per-session hook event log (one JSON line per hook event on every event the generated registry marks observable, written to /sessions/.jsonl, with SessionEnd retention by session count or age and an optional detached pre-prune command), a family of eight advisory *-audit hooks (API errors, config changes, instruction loads, permission denials, pre-compaction, skill usage, tool failures, and unsurfaced hook failures. The last also warns the user via systemMessage, since a hook that fails to launch enforces nothing and Claude Code surfaces the failure to nobody) that emit the shared hook-telemetry envelope, and a reference sink that routes envelopes under the same root: per session when the envelope carries a session id, else into the shared hook-events.jsonl the observability skill reads.", "author": { "name": "Melodic Software", diff --git a/plugins/claude-ops/CHANGELOG.md b/plugins/claude-ops/CHANGELOG.md index a8ff3f32f6..6e842a7141 100644 --- a/plugins/claude-ops/CHANGELOG.md +++ b/plugins/claude-ops/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to the `claude-ops` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.77.1] - 2026-09-30 + +### Changed + +- **The `hook-failure-audit` Stop row runs in shell form.** The detector that reports unsurfaced hook failures no longer depends on `node`, the launcher whose absence it must report. The README and `/claude-ops:setup` name the exception. + ## [0.77.0] - 2026-09-30 ### Changed diff --git a/plugins/claude-ops/README.md b/plugins/claude-ops/README.md index e230ce039d..b9b10dca6d 100644 --- a/plugins/claude-ops/README.md +++ b/plugins/claude-ops/README.md @@ -343,10 +343,11 @@ The audit hooks are Bash scripts (Git Bash on native Windows, so install [Git for Windows](https://code.claude.com/docs/en/setup#set-up-on-windows)) and use `jq`; without jq they fail open (no audit line is written). -**Node.js on PATH.** Every hook row starts through `node hooks/exec-bash.mjs`, which finds the -real Bash and runs the script. Claude Code's native binary neither ships nor uses Node.js -([setup](https://code.claude.com/docs/en/setup), fetched 2026-09-29), so without `node` on PATH -the hooks do not launch. `/claude-ops:setup check` reports whether `node` resolves. +**Node.js on PATH.** Every hook row except `hook-failure-audit` starts through +`node hooks/exec-bash.mjs`, which finds the real Bash and runs the script. Claude Code's native +binary neither ships nor uses Node.js ([setup](https://code.claude.com/docs/en/setup), fetched +2026-09-29), so without `node` on PATH those hooks do not launch. The `hook-failure-audit` Stop row +is shell form (`"shell": "bash"`) and needs no node, so it still reports the failed launches. `/claude-ops:setup check` reports whether `node` resolves. `audit-install-state` needs **Python 3.11+ only**. No PowerShell, no third-party packages, no `jq`. Its inventory, surface classification, filename-scheme resolution, retention resolution and diff --git a/plugins/claude-ops/hooks/hook-failure-audit.test.sh b/plugins/claude-ops/hooks/hook-failure-audit.test.sh index 5447ba9d33..fa3c9d64ae 100755 --- a/plugins/claude-ops/hooks/hook-failure-audit.test.sh +++ b/plugins/claude-ops/hooks/hook-failure-audit.test.sh @@ -664,4 +664,16 @@ assert_contains "injected failure names the hook on stderr" "$abort_err" \ abort_lines=$(printf '%s\n' "$abort_err" | grep -c . || true) assert_eq "injected failure writes one stderr line" "1" "$abort_lines" +# --- the Stop row is shell form: a launcher through node could not report a missing node --- +row="$(jq -c '[.hooks.Stop[].hooks[] | select((.command // "") | contains("hook-failure-audit.sh"))]' "$HOOK_DIR/hooks.json")" +assert_eq "one hook-failure-audit Stop row" "1" "$(jq 'length' <<<"$row")" +assert_eq "the row is shell-form bash with no args and no node" "true" \ + "$(jq '.[0] | .type == "command" and .shell == "bash" and (has("args") | not) and (.command | contains("node") | not)' <<<"$row")" +row_cmd="$(jq -r '.[0].command' <<<"$row")" +row_rc=0 +printf '%s' '{"session_id":"s","transcript_path":"/no/such.jsonl","hook_event_name":"Stop"}' \ + | CLAUDE_PLUGIN_ROOT="$(dirname "$HOOK_DIR")" CLAUDE_PLUGIN_DATA="$TEST_TMPDIR/row-data" \ + bash -c "$row_cmd" >/dev/null 2>&1 || row_rc=$? +assert_exit "the registered command line runs the hook and exits 0" 0 "$row_rc" + report diff --git a/plugins/claude-ops/hooks/hooks.json b/plugins/claude-ops/hooks/hooks.json index 3125b7ff49..603a74ee65 100644 --- a/plugins/claude-ops/hooks/hooks.json +++ b/plugins/claude-ops/hooks/hooks.json @@ -249,11 +249,8 @@ "hooks": [ { "type": "command", - "command": "node", - "args": [ - "${CLAUDE_PLUGIN_ROOT}/hooks/exec-bash.mjs", - "${CLAUDE_PLUGIN_ROOT}/hooks/hook-failure-audit.sh" - ], + "shell": "bash", + "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/hook-failure-audit.sh\"", "timeout": 10, "statusMessage": "Checking for unsurfaced hook failures..." } diff --git a/plugins/claude-ops/skills/setup/SKILL.md b/plugins/claude-ops/skills/setup/SKILL.md index 031285f464..cb4935d84c 100644 --- a/plugins/claude-ops/skills/setup/SKILL.md +++ b/plugins/claude-ops/skills/setup/SKILL.md @@ -103,8 +103,9 @@ modify anything. skill moved to the hook log root, so rows left in the old file are read by nothing. The skill-usage store and the OTEL store under `.claude/observability/` are not retired and produce no finding. -7. **Node.js for the hook launcher.** Every hook row starts through - `node "${CLAUDE_PLUGIN_ROOT}/hooks/exec-bash.mjs"`, so the hooks need `node` on PATH. Probe it +7. **Node.js for the hook launcher.** Every hook row except + `hook-failure-audit` (shell form, no node) starts through + `node "${CLAUDE_PLUGIN_ROOT}/hooks/exec-bash.mjs"`, so those hooks need `node` on PATH. Probe it through the Bash tool with `command -v node`, which does not depend on the launcher. Resolves: PASS. Does not resolve: FAIL, the hooks do not launch and record nothing; the remediation is the person installing Node.js (this skill installs nothing) and starting a fresh session. diff --git a/plugins/claude-ops/skills/setup/evals/evals.json b/plugins/claude-ops/skills/setup/evals/evals.json index e9a3f77cf8..2e856afd7e 100644 --- a/plugins/claude-ops/skills/setup/evals/evals.json +++ b/plugins/claude-ops/skills/setup/evals/evals.json @@ -81,7 +81,7 @@ "id": 7, "name": "reports-whether-node-resolves-for-the-hook-launcher", "prompt": "/claude-ops:setup check, on a host where `command -v node` finds nothing.", - "expected_output": "check probes `command -v node` through the Bash tool, not through a hook, and reports the Node.js row as FAIL: every hook row starts through node, so the hooks do not launch and record nothing. The remediation is the person installing Node.js and starting a fresh session; the skill installs nothing.", + "expected_output": "check probes `command -v node` through the Bash tool, not through a hook, and reports the Node.js row as FAIL: every hook row except `hook-failure-audit` starts through node, so those hooks do not launch and record nothing. The remediation is the person installing Node.js and starting a fresh session; the skill installs nothing.", "files": [], "expectations": [ "The probe is `command -v node` run through the Bash tool", diff --git a/plugins/disk-hygiene/.claude-plugin/plugin.json b/plugins/disk-hygiene/.claude-plugin/plugin.json index ab09f1c18f..0ad9c4e97d 100644 --- a/plugins/disk-hygiene/.claude-plugin/plugin.json +++ b/plugins/disk-hygiene/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "disk-hygiene", - "version": "0.36.0", + "version": "0.37.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", diff --git a/plugins/disk-hygiene/CHANGELOG.md b/plugins/disk-hygiene/CHANGELOG.md index 68ecb5fe4c..ea413b5c85 100644 --- a/plugins/disk-hygiene/CHANGELOG.md +++ b/plugins/disk-hygiene/CHANGELOG.md @@ -3,6 +3,12 @@ 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.37.0] - 2026-09-30 + +### Added + +- **A SessionStart notice warns when `node` is missing.** The hook rows launch through `node`, so a host without it skipped the destructive-command guard silently. A shell-form row now prints a system message and model context at session start when `node` is not on `PATH`. The README documents the row. + ## [0.36.0] - 2026-09-30 ### Added diff --git a/plugins/disk-hygiene/README.md b/plugins/disk-hygiene/README.md index eb406e91d8..ec787f2e57 100644 --- a/plugins/disk-hygiene/README.md +++ b/plugins/disk-hygiene/README.md @@ -65,9 +65,15 @@ at preview. Backups remain the recovery boundary for user data. ## Requirements and platform support -- Node.js on `PATH`. Every hook registration runs `node hooks/exec-bash.mjs`, and Claude Code's +- Node.js on `PATH`. Every guard and detector registration runs `node hooks/exec-bash.mjs`, and Claude Code's native binary neither ships nor uses Node ([Setup](https://code.claude.com/docs/en/setup)), so - without `node` no hook launches and no guard is enforced. + without `node` no hook launches and no guard is enforced. A `SessionStart` row in shell form + (`"shell": "bash"`, no `args`) runs `command -v node` and needs no node itself. When node is + absent it exits 0 with JSON: `systemMessage` shows the user a warning and `additionalContext` + tells the model that the destructive-delete guard cannot launch and enforces nothing. It prints + nothing when node is present. Basis: https://code.claude.com/docs/en/hooks, "SessionStart" + (plain stdout reaches Claude only, and exit-2 stderr reaches the user only) and "JSON output" + (`systemMessage` is a warning shown to the user). - Bash that `hooks/exec-bash.mjs` can find. The file's header comment lists the candidates in order for each platform: on Windows, `CLAUDE_CODE_GIT_BASH_PATH`, the Git for Windows install roots, then `PATH`; elsewhere, `PATH` first. The WSL relay (`System32\bash.exe`) is never used. @@ -164,7 +170,7 @@ the call itself, the same way the guard's watchdog answers "could not decide": | No Python resolves: skill-scoped belt, any Bash or PowerShell call | Denied (exit 2), reason on stderr | | No Python resolves: plugin-level gate, command naming `hygiene.py` (or an empty payload) | Denied (exit 2), reason on stderr | | No Python resolves: plugin-level gate, any other command its `if` rows let through | **Proceeds unchecked**, with a `systemMessage` and `additionalContext` notice once per session | -| `node` missing or no bash found: every hook | **Proceeds unchecked.** The hook fails to launch, which is non-blocking: the user sees a hook error notice, the guard is not enforced, and the model is not told. With no bash, the notice's first line is the launcher's `exec-bash: