Filed by an agent session after a live /disk-hygiene:clean run. This is a regression introduced by a deliberate fix, so it is framed against that fix rather than as a fresh complaint.
Summary
Issue #1640 fixed over-gating: the engine gate was denying a consumer's own engine-named script when its absolute path held a non-keep-class character. Issue #1611 fixed a related over-gate on the plugin's own test file. The resulting escape in _engine_gate_relevant() grants a pass to a marker-carrying word that resolves to an existing file other than the bundled engine (skills/clean/scripts/destructive_guard.py:489-493, rationale at :443-449), and the plugin-level gate then defers with no output.
That escape now also covers the plugin's own previous engines, which is an under-gate in the opposite direction.
Measured on this host
17 version directories under ~/.claude/plugins/cache/melodic-software/disk-hygiene/ — 0.3.0 through 0.10.2 — 16 carrying an intact engine (0.9.4's is absent). Script-verified: for all 15 non-current copies os.path.samefile(other, bundled) is False and each path resolves. So the always-on gate classifies the plugin's own previous engines as "provably a different file" and defers.
For comparison, the sibling rate-limit-guard has 8 cached versions, so this is the normal state of a cache, not an artifact of unusual churn.
Consequence
disk_hygiene_enabled: false (audit-only) is not enforced against a stale engine whenever the clean skill is not the active work, because the plugin-level gate is the only guard in that state and it defers. The skill-scoped belt would deny it, but only while the skill is active.
- Versions at or below 0.8.1 predate settings-based kill-switch enforcement entirely.
reference/safety-model.md concedes that before 0.9.0 the gate carried a bare ${user_config.*} token whose presence dropped the whole engine-gate hook on a default install. Those engines answer to nothing but their own containment.
- What still holds, and bounds the severity: the stale engine's own preview, approval-token, and platform blockers remain. So this is a kill-switch-and-human-prompt bypass, not an unbounded-delete bypass.
Reachability is documented, not hypothetical
Rendered skill content "enters the conversation as a single message and stays there for the rest of the session… Claude Code does not re-read the skill file on later turns" (https://code.claude.com/docs/en/skills), while "when a plugin updates mid-session, hook commands… keep using the previous version's path. Run /reload-plugins" (https://code.claude.com/docs/en/plugins-reference).
So a body rendered from version N can coexist with a guard registered from N+1, and every command the rendered body dictates names N's engine — which N+1's guard both fails to recognise as an allowed shape and declines to gate.
The same page promises the previous version's directory "remains on disk for about two weeks after an update before cleanup." Empirically that bound does not hold here: 0.3.0 is still present. The residual window is unbounded in practice.
Suggested fix
Narrow the "provably a different file" escape to exclude any path inside the plugin's own cache tree (<plugins>/cache/<marketplace>/<name>/). The guard already derives that prefix in _plugin_data_root_from_root() (destructive_guard.py:175-210). A consumer's tools/hygiene.py keeps its escape; the plugin's own siblings stop getting one. One predicate, and it does not touch the behaviour #1640 and #1611 were fixing.
Honest limit on this report
Both halves are independently verified — the version population by directory listing and script, and the samefile escape by direct evaluation. But the end-to-end consequence was never demonstrated: no run set the kill switch to false and watched a stale engine proceed.
We deliberately did not run that reproduction, because it means invoking a stale, deletion-capable engine with the safety switch off. If you want it demonstrated, scope it to scan or preview, which touch no deletion path. Otherwise please treat this as reachable-but-undemonstrated with both halves verified.
Filed by an agent session after a live
/disk-hygiene:cleanrun. This is a regression introduced by a deliberate fix, so it is framed against that fix rather than as a fresh complaint.Summary
Issue #1640 fixed over-gating: the engine gate was denying a consumer's own engine-named script when its absolute path held a non-keep-class character. Issue #1611 fixed a related over-gate on the plugin's own test file. The resulting escape in
_engine_gate_relevant()grants a pass to a marker-carrying word that resolves to an existing file other than the bundled engine (skills/clean/scripts/destructive_guard.py:489-493, rationale at:443-449), and the plugin-level gate then defers with no output.That escape now also covers the plugin's own previous engines, which is an under-gate in the opposite direction.
Measured on this host
17 version directories under
~/.claude/plugins/cache/melodic-software/disk-hygiene/— 0.3.0 through 0.10.2 — 16 carrying an intact engine (0.9.4's is absent). Script-verified: for all 15 non-current copiesos.path.samefile(other, bundled)isFalseand each path resolves. So the always-on gate classifies the plugin's own previous engines as "provably a different file" and defers.For comparison, the sibling
rate-limit-guardhas 8 cached versions, so this is the normal state of a cache, not an artifact of unusual churn.Consequence
disk_hygiene_enabled: false(audit-only) is not enforced against a stale engine whenever the clean skill is not the active work, because the plugin-level gate is the only guard in that state and it defers. The skill-scoped belt would deny it, but only while the skill is active.reference/safety-model.mdconcedes that before 0.9.0 the gate carried a bare${user_config.*}token whose presence dropped the whole engine-gate hook on a default install. Those engines answer to nothing but their own containment.Reachability is documented, not hypothetical
Rendered skill content "enters the conversation as a single message and stays there for the rest of the session… Claude Code does not re-read the skill file on later turns" (https://code.claude.com/docs/en/skills), while "when a plugin updates mid-session, hook commands… keep using the previous version's path. Run
/reload-plugins" (https://code.claude.com/docs/en/plugins-reference).So a body rendered from version N can coexist with a guard registered from N+1, and every command the rendered body dictates names N's engine — which N+1's guard both fails to recognise as an allowed shape and declines to gate.
The same page promises the previous version's directory "remains on disk for about two weeks after an update before cleanup." Empirically that bound does not hold here: 0.3.0 is still present. The residual window is unbounded in practice.
Suggested fix
Narrow the "provably a different file" escape to exclude any path inside the plugin's own cache tree (
<plugins>/cache/<marketplace>/<name>/). The guard already derives that prefix in_plugin_data_root_from_root()(destructive_guard.py:175-210). A consumer'stools/hygiene.pykeeps its escape; the plugin's own siblings stop getting one. One predicate, and it does not touch the behaviour #1640 and #1611 were fixing.Honest limit on this report
Both halves are independently verified — the version population by directory listing and script, and the
samefileescape by direct evaluation. But the end-to-end consequence was never demonstrated: no run set the kill switch tofalseand watched a stale engine proceed.We deliberately did not run that reproduction, because it means invoking a stale, deletion-capable engine with the safety switch off. If you want it demonstrated, scope it to
scanorpreview, which touch no deletion path. Otherwise please treat this as reachable-but-undemonstrated with both halves verified.