From f1381bed15a07f8d0c11102aa060e8379a990286 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:25:08 -0400 Subject: [PATCH 1/7] fix(disk-hygiene): split the engine gate's marker-free identity readings The marker-free branch of _engine_gate_relevant now dedupes its candidates and filters the two samefile readings separately: the as-written probe runs only on separator-carrying words, and the engine-directory probe runs on relative, non-empty, drive-less words, so D:foo never probes another drive. git log --oneline --graph --decorate origin/main drops from 24 probes to 7. Tests add a verdict differential against a frozen copy of the old branch (ordinary and link-gating commands, both tools, both kill-switch states), hard links inside and outside the engine directory, and counted probes. Refs #3527 Co-Authored-By: Claude Opus 5.5 --- .../skills/clean/scripts/destructive_guard.py | 75 +++++-- .../skills/clean/scripts/test_hygiene.py | 211 ++++++++++++++++++ 2 files changed, 264 insertions(+), 22 deletions(-) diff --git a/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py b/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py index 8e68624b8a..d5d7a22e17 100755 --- a/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py +++ b/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py @@ -20,12 +20,15 @@ stale network drive letter or UNC path referenced by an ordinary, unrelated Bash command) would not by itself explain an *uncaught* exception. Two things follow: (1) the strongest identified candidate for the 17s itself is -``_engine_gate_relevant``'s marker-free fallback, which calls -``os.path.samefile`` on every separator-containing word of *every* Bash/ +``_engine_gate_relevant``'s marker-free fallback, which runs on *every* Bash/ PowerShell command in *every* session (not only disk-hygiene commands) when -resolving the plugin-level engine gate — a slow or unreachable path argument -in an unrelated command is a real, user-reachable way to stall this hook for -longer than milliseconds; (2) empty stderr is not what an uncaught Python +resolving the plugin-level engine gate. It calls ``os.path.samefile`` on each +distinct separator-carrying word as written — a slow or unreachable path +argument in an unrelated command is a real, user-reachable way to stall this +hook for longer than milliseconds — and on each distinct relative, non-empty, +drive-less word joined to the engine's own directory, so a word that names no +path costs one probe inside that directory; (2) empty stderr is not what an +uncaught Python exception normally produces (the default handler writes a traceback), so an external kill (antivirus/EDR scanning the ``python3`` process, a transient OS resource issue) remains an open, unconfirmed possibility this module cannot @@ -721,12 +724,18 @@ def _engine_gate_relevant(command: str, tool_name: str = "Bash") -> bool: mere mention (expansions, operators, unparsable quoting) — fail closed into the gate; the belt's own rules then decide. - A path-like word (containing a separator) that is the SAME FILE as the - bundled engine — a symlink or hard link under any name — gates regardless - of its filename. Accepted residuals, all of the copy-evasion class the gate - can never close (a byte copy is a different file): a PATH-installed alias - with no separator, an alias inside a command the literal parser rejects - when the marker is absent, and a copied engine. This is a belt, not the + A word that is the SAME FILE as the bundled engine — a symlink or hard + link under any name — gates regardless of its filename. Without the + marker, each distinct word is read two ways: as written when it carries a + path separator (``/``, plus ``\\`` on Windows or under PowerShell), and + joined to the engine's own directory when it is relative, non-empty and + has no drive. A link beside the engine therefore gates invoked bare or as + ``./alias``. Accepted residuals, all of the copy-evasion class the gate + can never close (a byte copy is a different file): an alias outside the + engine's directory named with no separator (PATH-installed, or in the + working directory as ``python3 alias``), a drive-relative alias + (``D:alias``), an alias inside a command the literal parser rejects when + the marker is absent, and a copied engine. This is a belt, not the authority: an invocation smuggled past it still answers to the engine's own preview/approval-token containment (and to the skill-frontmatter belt for the rest of the session once that belt has registered). @@ -744,12 +753,17 @@ def _samefile(word: str) -> bool: except (OSError, ValueError): return False + def _in_engine_dir(word: str) -> bool: + return _samefile(os.path.join(bundled.parent, word)) + def _same_file_as_bundled(word: str) -> bool: """Whether ``word`` NAMES the bundled engine, under any spelling. - Read two ways: as written, and — when relative — against the ENGINE'S - OWN directory. The second reading is what closes the Windows - filename-alias class. Win32 discards trailing dots and spaces from a + Read two ways: as written (``_samefile``), and — when relative — + against the ENGINE'S OWN directory (``_in_engine_dir``). The + marker-free branch applies its own filter to each reading; the + marker-carrying branch uses both unfiltered. The second reading is + what closes the Windows filename-alias class. Win32 discards trailing dots and spaces from a filename and resolves `::$DATA` to the main stream, so `hygiene.py.`, `"hygiene.py "` and `hygiene.py::$DATA` all open the bundled engine while none of them has its basename; `cd && python @@ -762,18 +776,16 @@ def _same_file_as_bundled(word: str) -> bool: directory is the right base precisely because it is the directory such a command must `cd` into for the alias to run. """ - if _samefile(word): - return True - return not os.path.isabs(word) and _samefile(os.path.join(bundled.parent, word)) + return _samefile(word) or (not os.path.isabs(word) and _in_engine_dir(word)) allow_backslash = tool_name == "PowerShell" marker_candidates = _marker_tokens(command) if not any(_carries_marker(token) for token in marker_candidates): # No marker: the only relevant shape is a linked alias of the bundled - # engine invoked by path. Unparsable marker-free commands cannot fail - # closed (that would gate every command with an operator), so scan - # their whitespace tokens for separator-carrying words and identity- - # check those — a literal alias path gates even beside an operator. + # engine. Unparsable marker-free commands cannot fail closed (that + # would gate every command with an operator), so identity-check their + # whitespace tokens — a literal alias path gates even beside an + # operator. # # The path-legal tokens are scanned as well, and carry this branch's # weight now that a name merely CONTAINING the marker lands here: a @@ -782,6 +794,15 @@ def _same_file_as_bundled(word: str) -> bool: # `/tmp/test_hygiene.py;echo`, which resolves to nothing, so identity # would miss the engine under a name that is not the marker. Adding # candidates can only ever gate more, never less. + # + # Each distinct candidate gets two independently filtered readings. + # As written only when it carries a separator, so ordinary arguments + # (`status`, `--oneline`, `main`) are never probed against the guard's + # cwd. Against the engine's directory when it is relative, non-empty + # and drive-less: Windows joins `D:foo` onto drive D and discards the + # engine's directory, so that word would probe another drive and never + # this one. A word that names no path costs one probe inside the + # engine's directory and none elsewhere. candidates = list(marker_candidates) words = _literal_shell_words(command, allow_backslash=allow_backslash) candidates += ( @@ -789,7 +810,17 @@ def _same_file_as_bundled(word: str) -> bool: if words is None else list(words) ) - return any(_same_file_as_bundled(candidate) for candidate in candidates) + separators = "/\\" if allow_backslash or os.name == "nt" else "/" + return any( + (any(sep in candidate for sep in separators) and _samefile(candidate)) + or ( + bool(candidate) + and not os.path.isabs(candidate) + and not os.path.splitdrive(candidate)[0] + and _in_engine_dir(candidate) + ) + for candidate in dict.fromkeys(candidates) + ) words = _literal_shell_words(command, allow_backslash=allow_backslash) if words is None: # Marker present but not literally parseable (operators, compounds). diff --git a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py index 7aa395e924..4473e73802 100755 --- a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py +++ b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py @@ -8,6 +8,7 @@ import io import json import math +import ntpath import os import re import shlex @@ -19,6 +20,7 @@ import time import types import unittest +from collections.abc import Callable from contextlib import ( ExitStack, chdir as chdir_context, @@ -8126,6 +8128,215 @@ def test_engine_gate_catches_alias_beside_shell_operator(self) -> None: self.assertEqual("deny", result["hookSpecificOutput"]["permissionDecision"]) os.unlink(alias) + @staticmethod + def _old_marker_free_engine_gate( + real: Callable[[str, str], bool], + ) -> Callable[[str, str], bool]: + """The engine-gate predicate with the marker-free branch frozen as it was. + + Every candidate, duplicates included, was read as written and, when + relative, against the engine's directory. Marker-carrying commands + delegate to ``real``, whose branches for them are unchanged. + """ + + def relevant(command: str, tool_name: str = "Bash") -> bool: + tokens = guard._marker_tokens(command) + if any(guard._carries_marker(token) for token in tokens): + return real(command, tool_name) + bundled = guard._engine_script_path() + + def samefile(word: str) -> bool: + try: + return os.path.samefile(word, bundled) + except (OSError, ValueError): + return False + + words = guard._literal_shell_words( + command, allow_backslash=tool_name == "PowerShell" + ) + candidates = list(tokens) + ( + [token.strip("'\"") for token in command.split()] + if words is None + else list(words) + ) + return any( + samefile(candidate) + or ( + not os.path.isabs(candidate) + and samefile(os.path.join(bundled.parent, candidate)) + ) + for candidate in candidates + ) + + return relevant + + def _hard_link_or_skip(self, link: Path) -> Path: + """Hard-link the bundled engine at ``link``; skip, never copy, if refused. + + A copy is a different file, so a test that fell back to one would + assert nothing about identity. + """ + try: + os.link(SCRIPT_DIR / "hygiene.py", link) + except OSError as exc: # pragma: no cover - filesystem-dependent + self.skipTest(f"hard links unavailable here: {exc}") + self.addCleanup(link.unlink, missing_ok=True) + return link + + def test_engine_gate_marker_free_split_keeps_verdict_parity(self) -> None: + """Splitting the two identity readings moves no allow/ask/deny verdict. + + Each command runs through the real guard twice, once with the frozen + marker-free predicate patched in, for both tools and both kill-switch + states. The link commands are in the corpus so parity is also asserted + where the verdict gates, not only where both predicates defer. + """ + tag = os.getpid() + in_dir = self._hard_link_or_skip(SCRIPT_DIR / f"engine-alias-{tag}") + dashed = self._hard_link_or_skip(SCRIPT_DIR / f"-engine-alias-{tag}") + outside_dir = Path(tempfile.mkdtemp(dir=SCRIPT_DIR)) + self.addCleanup(shutil.rmtree, outside_dir, ignore_errors=True) + outside = self._hard_link_or_skip(outside_dir / "cleanup").as_posix() + script = (SCRIPT_DIR / "hygiene.py").resolve().as_posix() + scripts = SCRIPT_DIR.as_posix() + ordinary = ( + "git status --short", + "git log --oneline --graph --decorate origin/main", + "git --no-pager -c color.ui=never diff HEAD~1 -- src/app.py", + "git diff -- hygiene.py", + "npm run build -- --watch", + "npm install --save-dev typescript@5", + 'rg -n "foo bar" src/ --glob "*.py"', + "rg hygiene.py README.md", + "echo hello", + "echo 'a b' \"c d\"", + "ls -la /tmp && echo done", + "cat a.txt | grep -v x > out.txt", + "(cd sub; make -j4)", + 'for f in *.py; do echo "$f"; done', + "python3 -c 'print(1)'", + "./scripts/run.sh --flag ../other/dir", + "-la --color --", + "D:foo --bar", + "C:\\Users\\x\\file.txt", + "Get-ChildItem -Path C:\\Users -Recurse", + "Remove-Item -Recurse -Force .\\build", + 'Write-Host "hi"; Get-Location', + "& python .\\tools\\hygiene.py --help", + f"cd {scripts} && python hygiene.py. apply --plan p --token t", + f'python3 "{script}" scan --help', + self._engine_command("scan"), + self._engine_command("apply"), + ) + gating = ( + f"{in_dir.name} apply", + f"./{in_dir.name} apply", + f"python3 -- {dashed.name} apply", + f"cd {scripts} && python3 -- {dashed.name} apply", + outside, + f"{outside};echo done", + ) + real = guard._engine_gate_relevant + old = self._old_marker_free_engine_gate(real) + + def verdict(result: dict[str, Any] | None) -> str | None: + return ( + None + if result is None + else result["hookSpecificOutput"]["permissionDecision"] + ) + + with tempfile.TemporaryDirectory() as elsewhere, chdir_context(elsewhere): + for command in gating: + self.assertTrue(old(command, "Bash"), command) + self.assertTrue(real(command, "Bash"), command) + for command in ordinary + gating: + for tool in ("Bash", "PowerShell"): + for enabled in (True, False): + with self.subTest(command=command, tool=tool, on=enabled): + new = self.run_guard_engine_gate(command, tool, enabled) + with mock.patch.object(guard, "_engine_gate_relevant", old): + before = self.run_guard_engine_gate( + command, tool, enabled + ) + self.assertEqual(verdict(before), verdict(new)) + + def test_engine_gate_hard_link_in_engine_dir_gates_bare_and_dotted(self) -> None: + """A link beside the engine gates by bare name as well as by `./` path. + + The bare name carries no separator, so only the engine-directory + reading can see it; the run happens from an unrelated directory so the + as-written reading cannot supply the match instead. + """ + alias = self._hard_link_or_skip(SCRIPT_DIR / f"engine-alias-{os.getpid()}") + with tempfile.TemporaryDirectory() as elsewhere, chdir_context(elsewhere): + for command in (f"{alias.name} apply", f"./{alias.name} apply"): + result = self.run_guard_engine_gate(command, "Bash", enabled=False) + assert result is not None, command + self.assertEqual( + "deny", + result["hookSpecificOutput"]["permissionDecision"], + command, + ) + + def test_engine_gate_hard_link_outside_engine_dir_gates_by_absolute_path( + self, + ) -> None: + """A link elsewhere gates by its absolute path, alone or glued to `;`.""" + with tempfile.TemporaryDirectory(dir=SCRIPT_DIR) as tmp: + alias = self._hard_link_or_skip(Path(tmp) / "cleanup").as_posix() + for command in (alias, f"{alias};echo done"): + result = self.run_guard_engine_gate(command, "Bash", enabled=False) + assert result is not None, command + self.assertEqual( + "deny", + result["hookSpecificOutput"]["permissionDecision"], + command, + ) + + def test_engine_gate_marker_free_probes_stay_inside_the_engine_dir(self) -> None: + """Counted `os.path.samefile` calls for marker-free commands. + + Only a separator-carrying word is probed as written; every other probe + is a distinct word joined to the engine's directory. A flag costs one + such probe: a file named like a flag can sit beside the engine and run + as `python3 -- -alias`, so flags are not exempt. + """ + probes_before = 24 + probes_after = 7 + bundled = guard._engine_script_path() + engine_dir = os.fspath(bundled.parent) + real = guard._engine_gate_relevant + old = self._old_marker_free_engine_gate(real) + + def probes(predicate: Callable[[str, str], bool], command: str) -> list[str]: + with mock.patch.object( + guard.os.path, "samefile", wraps=os.path.samefile + ) as counted: + self.assertFalse(predicate(command, "Bash"), command) + return [os.fspath(call.args[0]) for call in counted.call_args_list] + + ordinary = "git log --oneline --graph --decorate origin/main" + self.assertEqual(probes_before, len(probes(old, ordinary))) + self.assertEqual(probes_after, len(probes(real, ordinary))) + self.assertLess(probes_after, probes_before) + for command in ( + "ls -la --color --", + "git --no-pager -c x", + "npm run build -- --watch", + ): + seen = probes(real, command) + self.assertEqual(len(dict.fromkeys(command.split())), len(seen), seen) + for path in seen: + self.assertEqual(engine_dir, os.path.dirname(path), command) + # Windows joins a drive-relative word onto its own drive and drops the + # base (`ntpath.join("C:\\eng", "D:foo") == "D:foo"`), so the old + # engine-directory reading probed drive D. Windows drive parsing is + # patched in so the check runs on every host. + with mock.patch.object(guard.os.path, "splitdrive", ntpath.splitdrive): + self.assertEqual([], probes(real, "D:foo")) + self.assertIn(os.path.join(engine_dir, "D:foo"), probes(old, "D:foo")) + def test_engine_gate_defers_consumer_windows_path_on_powershell(self) -> None: """Native consumer paths must defer on PowerShell, not fail closed (P2 r6). From 269ae0359dd7baa35dbb36b446d3f33e733a5fb1 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:30:31 -0400 Subject: [PATCH 2/7] chore(disk-hygiene): release 0.28.20 for the engine gate probe split Co-Authored-By: Claude Opus 5.5 --- plugins/disk-hygiene/.claude-plugin/plugin.json | 2 +- plugins/disk-hygiene/CHANGELOG.md | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/disk-hygiene/.claude-plugin/plugin.json b/plugins/disk-hygiene/.claude-plugin/plugin.json index eba7717009..f2a437dcd5 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.28.19", + "version": "0.28.20", "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 2e535d9ec2..6260f817e3 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.28.20] - 2026-09-29 + +### Fixed + +- **The engine gate probes far fewer files on commands that do not name the engine.** Without the `hygiene.py` marker, `_engine_gate_relevant` now reads each distinct word two ways with separate filters. The as-written identity probe runs only on words that carry a path separator, so ordinary arguments such as `status` or `--oneline` are no longer checked against the guard's working directory. The engine-directory reading still covers bare-word aliases inside the engine's directory, the shape that matters on shells that search the current directory before `PATH` (Windows `cmd`). Drive-qualified words such as `D:foo` are excluded from that reading, because Windows joins them onto drive D and drops the engine directory, so they could probe a dead drive. Candidates are deduplicated; the check is a pure predicate under `any()`, so this cannot change the result. The accepted residuals are unchanged apart from one that follows from the separator filter: an alias outside the engine's directory invoked by a bare name (for example a hard link in the working directory run as `python3 alias`) no longer gates. + ## [0.28.19] - 2026-09-29 ### Changed From 30aa5d98b25adf707ae8e4f3d3219b40dd4fd118 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:08:39 -0400 Subject: [PATCH 3/7] fix(disk-hygiene): read drive-less words against the engine directory on Windows The drive filter compared a word's drive to the engine's drive, so a bare word (empty drive) was skipped whenever the engine sat on a lettered drive. A link beside the engine invoked by bare name or as `hygiene.py.` stopped gating on Windows. Accept an empty drive as well, and cover the bare word in the patched-Windows same-drive test. Co-Authored-By: Claude Opus 5.5 --- .../skills/clean/scripts/destructive_guard.py | 2 +- .../disk-hygiene/skills/clean/scripts/test_hygiene.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py b/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py index e7e2748c65..356af57277 100755 --- a/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py +++ b/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py @@ -821,7 +821,7 @@ def _same_file_as_bundled(word: str) -> bool: or ( bool(candidate) and not os.path.isabs(candidate) - and os.path.splitdrive(candidate)[0].casefold() == engine_drive + and os.path.splitdrive(candidate)[0].casefold() in {"", engine_drive} and _in_engine_dir(candidate) ) for candidate in dict.fromkeys(candidates) diff --git a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py index 6905014b4a..1336de3cc7 100755 --- a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py +++ b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py @@ -9005,12 +9005,13 @@ def probes(predicate: Callable[[str, str], bool], command: str) -> list[str]: self.assertIn(os.path.join(engine_dir, "D:foo"), probes(old, "D:foo")) def test_engine_gate_reads_a_same_drive_word_against_the_engine_dir(self) -> None: - """`C:alias` beside an engine on `C:` still gates; `D:alias` is not read. + """`alias` and `C:alias` beside an engine on `C:` gate; `D:alias` is not read. `ntpath.join("C:\\eng", "C:alias")` is `C:\\eng\\alias`, so a link beside the engine invoked as `C:alias` gated before the split and must - still, whatever the drive letter's case. Only a word on another drive - drops the engine's directory. Windows path handling is patched in so + still, whatever the drive letter's case; a bare `alias` has no drive + and joins the same way. Only a word on another drive drops the + engine's directory. Windows path handling is patched in so the check runs on every host. """ engine = PureWindowsPath("C:\\eng\\hygiene.py") @@ -9030,7 +9031,7 @@ def samefile(word: object, other: object) -> bool: mock.patch.object(guard.os.path, "isabs", ntpath.isabs), mock.patch.object(guard.os.path, "samefile", samefile), ): - for command in ("C:alias apply", "c:alias apply"): + for command in ("alias apply", "C:alias apply", "c:alias apply"): self.assertTrue(old(command, "Bash"), command) self.assertTrue(real(command, "Bash"), command) probed.clear() From 76a38053fdb442d28d03d3f565abe42640a7b292 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:24:05 -0400 Subject: [PATCH 4/7] test(disk-hygiene): pick a foreign drive that differs from the engine's The probe-count test hardcoded D: as the other drive, which is the engine's own drive on the Windows runner. Derive a drive that is not the engine's. Co-Authored-By: Claude Opus 5.5 --- plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py index 1336de3cc7..7247214ef8 100755 --- a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py +++ b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py @@ -9001,8 +9001,10 @@ def probes(predicate: Callable[[str, str], bool], command: str) -> list[str]: # engine-directory reading probed drive D. Windows drive parsing is # patched in so the check runs on every host. with mock.patch.object(guard.os.path, "splitdrive", ntpath.splitdrive): - self.assertEqual([], probes(real, "D:foo")) - self.assertIn(os.path.join(engine_dir, "D:foo"), probes(old, "D:foo")) + engine_drive = ntpath.splitdrive(engine_dir)[0].casefold() + other = "Z:foo" if engine_drive == "d:" else "D:foo" + self.assertEqual([], probes(real, other)) + self.assertIn(os.path.join(engine_dir, other), probes(old, other)) def test_engine_gate_reads_a_same_drive_word_against_the_engine_dir(self) -> None: """`alias` and `C:alias` beside an engine on `C:` gate; `D:alias` is not read. From 687f87487aba1dad15c8c464d2ac4895e449d5e1 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:56:30 -0400 Subject: [PATCH 5/7] test(disk-hygiene): pin the bare-name residual and the nt separator arm Add a differential for a bare-name hard link outside the engine directory, the accepted residual of the separator filter, and a test that a backslash word is probed as written under os.name nt for Bash. Co-Authored-By: Claude Opus 5.5 --- .../skills/clean/scripts/test_hygiene.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py index 7247214ef8..d1ebaeead9 100755 --- a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py +++ b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py @@ -8961,6 +8961,44 @@ def test_engine_gate_hard_link_outside_engine_dir_gates_by_absolute_path( command, ) + def test_engine_gate_bare_link_outside_engine_dir_defers(self) -> None: + """A bare-name link outside the engine directory no longer gates. + + The as-written probe now needs a separator, so a link in the working + directory invoked as `cleanup` is the accepted residual. The old + predicate gated it; the differential pins where the two diverge. + """ + real = guard._engine_gate_relevant + old = self._old_marker_free_engine_gate(real) + with tempfile.TemporaryDirectory(dir=SCRIPT_DIR) as tmp: + self._hard_link_or_skip(Path(tmp) / "cleanup") + self.addCleanup(os.chdir, os.getcwd()) + os.chdir(tmp) + command = "cleanup apply test_hygiene.py" + self.assertTrue(old(command, "Bash")) + self.assertFalse(real(command, "Bash")) + self.assertTrue(real(f"./cleanup {command}", "Bash")) + + def test_engine_gate_backslash_word_is_probed_as_written_on_nt(self) -> None: + """Under `os.name == "nt"` a backslash counts as a separator for Bash.""" + probed: list[str] = [] + + def samefile(word: object, other: object) -> bool: + probed.append(os.fspath(word)) + return False + + command = "'sub\\alias' apply" + engine = guard._engine_script_path() + for name, expected in (("nt", True), ("posix", False)): + probed.clear() + with ( + mock.patch.object(guard.os, "name", name), + mock.patch.object(guard, "_engine_script_path", return_value=engine), + mock.patch.object(guard.os.path, "samefile", samefile), + ): + guard._engine_gate_relevant(command, "Bash") + self.assertEqual(expected, "sub\\alias" in probed, name) + def test_engine_gate_marker_free_probes_stay_inside_the_engine_dir(self) -> None: """Counted `os.path.samefile` calls for marker-free commands. From d351f0de10438497109a0e7abfebce5ce3312283 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:03:15 -0400 Subject: [PATCH 6/7] fix(disk-hygiene): probe every marker-free word as written in the engine gate The marker-free branch of _engine_gate_relevant probed the as-written reading only for words that carry a path separator. A hard link to the engine at a bare name outside the engine's directory (`python3 alias` in the working directory) then matched neither reading and no longer gated, unarming the kill switch for it. Restore the unconditional as-written probe for every candidate. Dedupe and the drive rule on the engine-directory reading stay: `git log --oneline --graph --decorate origin/main` probes 12 files instead of 24. The hard-link test for a bare name outside the engine directory now asserts the gate fires, the separator-only backslash test is removed, the counted probes and the drive-qualified cases are recomputed, and the frozen baseline predicate mirrors main's PowerShell string-data blanking and identity pre-check. Released as 0.34.5 above main's 0.34.4. Refs #3527 Co-Authored-By: Claude Sonnet 5.5 --- .../disk-hygiene/.claude-plugin/plugin.json | 2 +- plugins/disk-hygiene/CHANGELOG.md | 18 +++ .../skills/clean/scripts/destructive_guard.py | 59 ++++--- .../skills/clean/scripts/test_hygiene.py | 147 +++++++++--------- 4 files changed, 124 insertions(+), 102 deletions(-) diff --git a/plugins/disk-hygiene/.claude-plugin/plugin.json b/plugins/disk-hygiene/.claude-plugin/plugin.json index ad2c53e7aa..76b93370ca 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.34.4", + "version": "0.34.5", "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 30c3751b16..8d95edd328 100644 --- a/plugins/disk-hygiene/CHANGELOG.md +++ b/plugins/disk-hygiene/CHANGELOG.md @@ -3,6 +3,24 @@ 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.34.5] - 2026-09-30 + +### Fixed + +- **The engine gate probes half as many files on commands that do not name the engine** + ([#3527](https://github.com/melodic-software/claude-code-plugins/issues/3527)). Without the + `hygiene.py` marker, `_engine_gate_relevant` now deduplicates its candidates, so each distinct + word is probed once as written and once joined to the engine's own directory, so + `git log --oneline --graph --decorate origin/main` drops from 24 probes to 12. The + engine-directory reading skips a word qualified with a drive other than the engine's, such as + `D:foo`: Windows joins it onto that drive and drops the engine's directory, so the reading only + repeated the as-written probe of the same path. A bare `alias` and `C:alias` beside an engine on + `C:` still read against the engine's directory. The as-written reading stays unconditional, + because a bare name reaches a link in the working directory (`python3 alias`) and a separator + filter would have stopped gating it. Flags and other non-path words are therefore still probed + against the working directory, so an unreachable path given as a word can still stall the hook, + and the accepted residuals are unchanged. + ## [0.34.4] - 2026-09-30 ### Fixed diff --git a/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py b/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py index 8731325eef..479b4a1ce8 100755 --- a/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py +++ b/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py @@ -23,13 +23,14 @@ ``_engine_gate_relevant``'s marker-free fallback, which runs on *every* Bash/ PowerShell command in *every* session (not only disk-hygiene commands) when resolving the plugin-level engine gate. It calls ``os.path.samefile`` on each -distinct separator-carrying whitespace token (or literal shell word) as -written — a slow or unreachable path argument in an unrelated command is a -real, user-reachable way to stall this hook for longer than milliseconds — and -on each distinct relative, non-empty word not on another drive, joined to the -engine's own directory, so a word that names no path costs one probe inside -that directory; (2) empty stderr is not what an uncaught Python -exception normally produces (the default handler writes a traceback), so an +distinct whitespace token (or literal shell word) as written — a slow or +unreachable path argument in an unrelated command is a real, user-reachable +way to stall this hook for longer than milliseconds, and the as-written probe +stays unfiltered because a bare name reaches a link in the working directory — +and on each distinct relative, non-empty word with no drive or the engine's +own, joined to the engine's own directory; (2) empty stderr is not what an +uncaught Python exception normally produces (the default handler writes a +traceback), so an external kill (antivirus/EDR scanning the ``python3`` process, a transient OS resource issue) remains an open, unconfirmed possibility this module cannot fix from inside the interpreter. What IS fixable and is fixed here: the @@ -851,16 +852,14 @@ def _engine_gate_relevant(command: str, tool_name: str = "Bash") -> bool: A word that is the SAME FILE as the bundled engine — a symlink or hard link under any name — gates regardless of its filename. Without the marker, each distinct whitespace token (or ``_literal_shell_words`` word) is read two - ways: as written when it carries a path separator (``/``, plus ``\\`` on - Windows or under PowerShell), and joined to the engine's own directory when - it is relative, non-empty and not qualified with another drive. A link - beside the engine therefore gates invoked bare or as ``./alias``, and + ways: as written, and joined to the engine's own directory when it is + relative, non-empty and has no drive or the engine's own. A link in the + working directory therefore gates invoked bare (``python3 alias``) as well + as by path, a link beside the engine gates bare or as ``./alias``, and ``C:alias`` reads against the engine's directory on the engine's own drive. Accepted residuals, all of the copy-evasion class the gate can never close - (a byte copy is a different file): an alias outside the engine's directory - named with no separator (PATH-installed, or in the working directory as - ``python3 alias``), an alias qualified with a drive other than the engine's - (``D:alias``), an alias inside a command the literal parser rejects when the + (a byte copy is a different file): a PATH-installed alias with no + separator, an alias inside a command the literal parser rejects when the marker is absent, and a copied engine. This is a belt, not the authority: an invocation smuggled past it still answers to the engine's own preview/approval-token containment (and to the skill-frontmatter belt for @@ -887,7 +886,7 @@ def _same_file_as_bundled(word: str) -> bool: Read two ways: as written (``_samefile``), and — when relative — against the ENGINE'S OWN directory (``_in_engine_dir``). The - marker-free branch applies its own filter to each reading; the + marker-free branch filters the second reading by drive; the marker-carrying branch uses both unfiltered. The second reading is what closes the Windows filename-alias class. Win32 discards trailing dots and spaces from a filename and resolves `::$DATA` to the main @@ -921,9 +920,10 @@ def _same_file_as_bundled(word: str) -> bool: marker_candidates = _marker_tokens(command) if not any(_carries_marker(token) for token in marker_candidates): # No marker: the only relevant shape is a linked alias of the bundled - # engine. Unparsable marker-free commands cannot fail closed (that - # would gate every command with an operator), so identity-check their - # whitespace tokens — a literal alias path gates even beside an + # engine invoked by path. Unparsable marker-free commands cannot fail + # closed (that would gate every command with an operator), so scan + # their whitespace tokens and identity-check every one, not only + # separator-carrying words — a literal alias path gates even beside an # operator. # # The path-legal tokens are scanned as well, and carry this branch's @@ -934,16 +934,14 @@ def _same_file_as_bundled(word: str) -> bool: # would miss the engine under a name that is not the marker. Adding # candidates can only ever gate more, never less. # - # Each distinct candidate gets two independently filtered readings. - # As written only when it carries a separator, so ordinary arguments - # (`status`, `--oneline`, `main`) are never probed against the guard's - # cwd. Against the engine's directory when it is relative, non-empty - # and not qualified with another drive: Windows joins `D:foo` onto drive - # D and discards the engine's directory, so that word would probe - # another drive and never this one, while `C:foo` on the engine's own - # drive still resolves inside the engine's directory. A word that names - # no path costs one probe inside the engine's directory and none - # elsewhere. + # Each distinct candidate is read two ways. As written, always: a bare + # name reaches a link in the shell's working directory (`python3 + # alias`), so no word is skipped. Against the engine's directory when it + # is relative, non-empty and has no drive or the engine's own: Windows + # joins `D:foo` onto drive D and discards the engine's directory, so + # that reading would repeat the as-written probe of the same path, while + # `C:foo` on the engine's own drive still resolves inside the engine's + # directory. candidates = list(marker_candidates) words = _literal_shell_words(command, allow_backslash=allow_backslash) candidates += ( @@ -951,10 +949,9 @@ def _same_file_as_bundled(word: str) -> bool: if words is None else list(words) ) - separators = "/\\" if allow_backslash or os.name == "nt" else "/" engine_drive = os.path.splitdrive(bundled.parent)[0].casefold() return any( - (any(sep in candidate for sep in separators) and _samefile(candidate)) + _samefile(candidate) or ( bool(candidate) and not os.path.isabs(candidate) diff --git a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py index a91d241d69..7f928300ea 100755 --- a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py +++ b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py @@ -10292,15 +10292,14 @@ def _old_marker_free_engine_gate( ) -> Callable[[str, str], bool]: """The engine-gate predicate with the marker-free branch frozen as it was. - Every candidate, duplicates included, was read as written and, when - relative, against the engine's directory. Marker-carrying commands + On PowerShell, string data is blanked and identity is read on the + command as written before the blanked command is classified. Then every + candidate, duplicates included, is read as written and, when relative, + against the engine's directory. Commands that still carry the marker delegate to ``real``, whose branches for them are unchanged. """ def relevant(command: str, tool_name: str = "Bash") -> bool: - tokens = guard._marker_tokens(command) - if any(guard._carries_marker(token) for token in tokens): - return real(command, tool_name) bundled = guard._engine_script_path() def samefile(word: str) -> bool: @@ -10309,11 +10308,27 @@ def samefile(word: str) -> bool: except (OSError, ValueError): return False - words = guard._literal_shell_words( - command, allow_backslash=tool_name == "PowerShell" + powershell = tool_name == "PowerShell" + data_free = ( + guard._powershell_without_string_data(command) if powershell else None ) + scanned = command if data_free is None else data_free + tokens = guard._marker_tokens(scanned) + if any(guard._carries_marker(token) for token in tokens): + return real(command, tool_name) + if data_free is not None and any( + samefile(candidate) + or ( + guard._carries_marker(token) + and guard._within_plugin_cache_family(candidate) + ) + for token, word in guard._marker_tokens_with_words(command) + for candidate in (token, word) + ): + return True + words = guard._literal_shell_words(scanned, allow_backslash=powershell) candidates = list(tokens) + ( - [token.strip("'\"") for token in command.split()] + [token.strip("'\"") for token in scanned.split()] if words is None else list(words) ) @@ -10342,12 +10357,14 @@ def _hard_link_or_skip(self, link: Path) -> Path: return link def test_engine_gate_marker_free_split_keeps_verdict_parity(self) -> None: - """Splitting the two identity readings moves no allow/ask/deny verdict. + """Deduping candidates and filtering the engine-directory reading move no verdict. Each command runs through the real guard twice, once with the frozen - marker-free predicate patched in, for both tools and both kill-switch - states. The link commands are in the corpus so parity is also asserted - where the verdict gates, not only where both predicates defer. + baseline patched in, for both tools and both kill-switch states. The + link commands are in the corpus so parity is also asserted where the + verdict gates, not only where both predicates defer. The PowerShell row + names the link inside a string literal, which the baseline blanks + before it reads candidates. """ tag = os.getpid() in_dir = self._hard_link_or_skip(SCRIPT_DIR / f"engine-alias-{tag}") @@ -10380,6 +10397,7 @@ def test_engine_gate_marker_free_split_keeps_verdict_parity(self) -> None: "Get-ChildItem -Path C:\\Users -Recurse", "Remove-Item -Recurse -Force .\\build", 'Write-Host "hi"; Get-Location', + f"Write-Host '{in_dir.name}'", "& python .\\tools\\hygiene.py --help", f"cd {scripts} && python hygiene.py. apply --plan p --token t", f'python3 "{script}" scan --help', @@ -10452,54 +10470,36 @@ def test_engine_gate_hard_link_outside_engine_dir_gates_by_absolute_path( command, ) - def test_engine_gate_bare_link_outside_engine_dir_defers(self) -> None: - """A bare-name link outside the engine directory no longer gates. + def test_engine_gate_bare_link_outside_engine_dir_gates(self) -> None: + """A link in the working directory gates by bare name, outside the engine dir. - The as-written probe now needs a separator, so a link in the working - directory invoked as `cleanup` is the accepted residual. The old - predicate gated it; the differential pins where the two diverge. + Only the as-written reading can see it, so a word with no separator + must be probed as written. `test_hygiene.py` is a decoy that keeps the + command in the marker-free branch. """ real = guard._engine_gate_relevant old = self._old_marker_free_engine_gate(real) with tempfile.TemporaryDirectory(dir=SCRIPT_DIR) as tmp: self._hard_link_or_skip(Path(tmp) / "cleanup") - self.addCleanup(os.chdir, os.getcwd()) - os.chdir(tmp) - command = "cleanup apply test_hygiene.py" - self.assertTrue(old(command, "Bash")) - self.assertFalse(real(command, "Bash")) - self.assertTrue(real(f"./cleanup {command}", "Bash")) - - def test_engine_gate_backslash_word_is_probed_as_written_on_nt(self) -> None: - """Under `os.name == "nt"` a backslash counts as a separator for Bash.""" - probed: list[str] = [] - - def samefile(word: object, other: object) -> bool: - probed.append(os.fspath(word)) - return False - - command = "'sub\\alias' apply" - engine = guard._engine_script_path() - for name, expected in (("nt", True), ("posix", False)): - probed.clear() - with ( - mock.patch.object(guard.os, "name", name), - mock.patch.object(guard, "_engine_script_path", return_value=engine), - mock.patch.object(guard.os.path, "samefile", samefile), - ): - guard._engine_gate_relevant(command, "Bash") - self.assertEqual(expected, "sub\\alias" in probed, name) + with chdir_context(tmp): + command = "cleanup apply test_hygiene.py" + self.assertTrue(old(command, "Bash")) + for spelled in (command, f"./{command}"): + self.assertTrue(real(spelled, "Bash"), spelled) + result = self.run_guard_engine_gate(command, "Bash", enabled=False) + assert result is not None + self.assertEqual("deny", result["hookSpecificOutput"]["permissionDecision"]) - def test_engine_gate_marker_free_probes_stay_inside_the_engine_dir(self) -> None: + def test_engine_gate_marker_free_probes_each_distinct_word_twice(self) -> None: """Counted `os.path.samefile` calls for marker-free commands. - Only a separator-carrying word is probed as written; every other probe - is a distinct word joined to the engine's directory. A flag costs one - such probe: a file named like a flag can sit beside the engine and run - as `python3 -- -alias`, so flags are not exempt. + Each distinct word is probed once as written and once joined to the + engine's directory, flags included: a link named like a flag can sit in + the working directory or beside the engine and run as `python3 -- -alias`. + The baseline probed every duplicate candidate too. """ probes_before = 24 - probes_after = 7 + probes_after = 12 bundled = guard._engine_script_path() engine_dir = os.fspath(bundled.parent) real = guard._engine_gate_relevant @@ -10521,29 +10521,35 @@ def probes(predicate: Callable[[str, str], bool], command: str) -> list[str]: "git --no-pager -c x", "npm run build -- --watch", ): - seen = probes(real, command) - self.assertEqual(len(dict.fromkeys(command.split())), len(seen), seen) - for path in seen: - self.assertEqual(engine_dir, os.path.dirname(path), command) + expected = [ + path + for word in dict.fromkeys(command.split()) + for path in (word, os.path.join(engine_dir, word)) + ] + self.assertEqual(expected, probes(real, command), command) # Windows joins a drive-relative word onto its own drive and drops the - # base (`ntpath.join("C:\\eng", "D:foo") == "D:foo"`), so the old - # engine-directory reading probed drive D. Windows drive parsing is - # patched in so the check runs on every host. + # base (`ntpath.join("C:\\eng", "D:foo") == "D:foo"`), so the + # engine-directory reading only repeated the as-written probe of drive + # D. Windows drive parsing is patched in so the check runs on every + # host. with mock.patch.object(guard.os.path, "splitdrive", ntpath.splitdrive): engine_drive = ntpath.splitdrive(engine_dir)[0].casefold() other = "Z:foo" if engine_drive == "d:" else "D:foo" - self.assertEqual([], probes(real, other)) + self.assertEqual([other], probes(real, other)) self.assertIn(os.path.join(engine_dir, other), probes(old, other)) - def test_engine_gate_reads_a_same_drive_word_against_the_engine_dir(self) -> None: - """`alias` and `C:alias` beside an engine on `C:` gate; `D:alias` is not read. - - `ntpath.join("C:\\eng", "C:alias")` is `C:\\eng\\alias`, so a link - beside the engine invoked as `C:alias` gated before the split and must - still, whatever the drive letter's case; a bare `alias` has no drive - and joins the same way. Only a word on another drive drops the - engine's directory. Windows path handling is patched in so - the check runs on every host. + def test_engine_gate_reads_drive_less_and_same_drive_words_against_the_engine_dir( + self, + ) -> None: + """`alias` and `C:alias` beside an engine on `C:` gate; `D:alias` is probed once. + + A bare relative `alias` has no drive and `ntpath.join("C:\\eng", + "C:alias")` is `C:\\eng\\alias`, so a link beside the engine invoked + either way gated before the filter and must still, whatever the drive + letter's case. Only a word on another drive drops the engine's + directory, so its engine-directory reading would repeat the as-written + probe and is skipped. Windows path handling is patched in so the check + runs on every host. """ engine = PureWindowsPath("C:\\eng\\hygiene.py") link = "C:\\eng\\alias" @@ -10563,14 +10569,15 @@ def samefile(word: object, other: object) -> bool: mock.patch.object(guard.os.path, "samefile", samefile), ): for command in ("alias apply", "C:alias apply", "c:alias apply"): - self.assertTrue(old(command, "Bash"), command) - self.assertTrue(real(command, "Bash"), command) + with self.subTest(command=command): + self.assertTrue(old(command, "Bash")) + self.assertTrue(real(command, "Bash")) probed.clear() self.assertFalse(old("D:alias apply", "Bash")) - self.assertIn("D:alias", probed) + self.assertEqual(4, probed.count("D:alias")) probed.clear() self.assertFalse(real("D:alias apply", "Bash")) - self.assertEqual([], [path for path in probed if path.startswith("D:")]) + self.assertEqual(1, probed.count("D:alias")) def test_engine_gate_defers_consumer_windows_path_on_powershell(self) -> None: """Native consumer paths must defer on PowerShell, not fail closed (P2 r6). From 1ff3eb1ce1d12277001e4c87d319c41bcb8d3310 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:13:00 -0400 Subject: [PATCH 7/7] docs(disk-hygiene): restore the other-drive alias residual in the engine gate disclosure Co-Authored-By: Claude Sonnet 5.5 --- plugins/disk-hygiene/CHANGELOG.md | 5 +++-- .../disk-hygiene/skills/clean/scripts/destructive_guard.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/disk-hygiene/CHANGELOG.md b/plugins/disk-hygiene/CHANGELOG.md index 7310702600..eac5d2aeb9 100644 --- a/plugins/disk-hygiene/CHANGELOG.md +++ b/plugins/disk-hygiene/CHANGELOG.md @@ -18,8 +18,9 @@ All notable changes to the `disk-hygiene` plugin are documented here. Format fol `C:` still read against the engine's directory. The as-written reading stays unconditional, because a bare name reaches a link in the working directory (`python3 alias`) and a separator filter would have stopped gating it. Flags and other non-path words are therefore still probed - against the working directory, so an unreachable path given as a word can still stall the hook, - and the accepted residuals are unchanged. + against the working directory, so an unreachable path given as a word can still stall the hook. + A link invoked with another drive's qualifier (`D:alias` beside an engine on `C:`) still does not + gate; the other accepted residuals are unchanged. ## [0.35.0] - 2026-09-30 diff --git a/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py b/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py index 96e1c54f38..138cf67094 100755 --- a/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py +++ b/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py @@ -868,7 +868,8 @@ def _engine_gate_relevant(command: str, tool_name: str = "Bash") -> bool: Accepted residuals, all of the copy-evasion class the gate can never close (a byte copy is a different file): a PATH-installed alias with no separator, an alias inside a command the literal parser rejects when the - marker is absent, and a copied engine. This is a belt, not the + marker is absent, an alias qualified with a drive other than the + engine's (``D:alias``), and a copied engine. This is a belt, not the authority: an invocation smuggled past it still answers to the engine's own preview/approval-token containment (and to the skill-frontmatter belt for the rest of the session once that belt has registered).