Skip to content

disk-hygiene: the "provably a different file" escape added in #1640 lets the plugin's own stale engines bypass the kill switch #1805

Description

@kyle-sexton

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions