From ec673cc0e04ad28a047295b2f82bf15822b8a539 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:19:10 -0400 Subject: [PATCH 1/4] fix(disk-hygiene): accept braces inside whole-word quotes in the engine gate parser _literal_shell_words rejected '{' and '}' before looking at quoting, so a quoted GUID path segment could never form an exact engine call. Braces are literal inside single and double quotes, so they are now accepted only inside a whole-word quoted span; every other expansion or operator character stays rejected anywhere, and unquoted braces still fail. _unparsable_reason no longer blames a quoted brace. Refs: #5641 Co-Authored-By: Claude Opus 5.5 --- .../skills/clean/scripts/destructive_guard.py | 18 +++- .../skills/clean/scripts/test_hygiene.py | 84 +++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py b/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py index 87349ea350..9cd565e63c 100755 --- a/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py +++ b/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py @@ -67,6 +67,15 @@ _SHELL_EXPANSION_OR_OPERATOR_CHARS = frozenset("{}$*?[]~`()<>;|&\r\n\t!#") +# A whole-word quoted span: a quote at a word start, no quote inside, the same +# quote closing it, then a space or the end. Braces are inert inside one. +_WHOLE_WORD_QUOTED = re.compile(r"(?['\"])[^'\"]*(?P=q)(?= |$)") +_BRACES = str.maketrans("", "", "{}") + + +def _without_quoted_braces(command: str) -> str: + """``command`` with ``{`` and ``}`` dropped from inside whole-word quotes.""" + return _WHOLE_WORD_QUOTED.sub(lambda m: m.group().translate(_BRACES), command) def decision(value: str, reason: str) -> dict[str, object]: @@ -181,10 +190,13 @@ def _literal_shell_words( ``allow_backslash`` permits ``\\`` inside words for surfaces where it is a path separator rather than an escape character (PowerShell commands); the - Bash default keeps rejecting it. + Bash default keeps rejecting it. ``{`` and ``}`` are accepted only inside a + whole-word quote, where they are literal; every other expansion or operator + character is rejected wherever it sits. """ if not command or any( - value in _SHELL_EXPANSION_OR_OPERATOR_CHARS for value in command + value in _SHELL_EXPANSION_OR_OPERATOR_CHARS + for value in _without_quoted_braces(command) ): return None words: list[str] = [] @@ -2178,7 +2190,7 @@ def _bash_allowlist_disclosure(authority: str | None) -> str: def _unparsable_reason(command: str) -> str: """Name the first thing in ``command`` that ``_literal_shell_words`` rejects.""" - for char in command: + for char in _without_quoted_braces(command): if char in _OPERATOR_LABELS: culprit = f"{_OPERATOR_LABELS[char]} ({char!r})" break diff --git a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py index e58d616247..a19e75620e 100755 --- a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py +++ b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py @@ -10564,6 +10564,58 @@ def test_engine_mismatch_reason_names_only_the_operator_present(self) -> None: if other != label: self.assertNotIn(phrase, reason) + def test_unparsable_reason_blames_an_unquoted_brace_never_a_quoted_one( + self, + ) -> None: + expansion = "a substitution or expansion" + for command, blamed in ( + ("a {b,c}", "'{'"), + ("a '{b}' {c}", "'{'"), + ("a '{b}' $(x)", "'$'"), + ('a "{b}" | c', "a pipe"), + ): + with self.subTest(command=command): + self.assertIn(blamed, guard._unparsable_reason(command)) + self.assertIn(expansion, guard._unparsable_reason("a {b,c}")) + for command in ("a '{b}' c\\d", "a '{b}' x'y'", 'a "{b}" \t'): + with self.subTest(command=command): + reason = guard._unparsable_reason(command) + self.assertNotIn("'{'", reason) + self.assertNotIn("'}'", reason) + self.assertNotIn(expansion, reason) + + def test_literal_parser_accepts_braces_only_inside_whole_word_quotes( + self, + ) -> None: + parse = guard._literal_shell_words + self.assertEqual(["a", "{b}"], parse("a '{b}'")) + self.assertEqual(["a", "x{y} z"], parse('a "x{y} z"')) + self.assertEqual(["a", "{b}", "c"], parse("a '{b}' c", allow_backslash=True)) + self.assertEqual(["a", "C:\\{g}"], parse('a "C:\\{g}"', allow_backslash=True)) + for command in ( + "a {b}", + "a x{b}", + "a '{b}'x", + "a x'{b}'", + "a '{b}' {c}", + "a '{b}", + 'a "${x}"', + 'a "$(x)"', + 'a "`x`"', + "a '$x{b}'", + "a '{b}' $x", + ): + with self.subTest(command=command): + self.assertIsNone(parse(command)) + + def test_quoted_braces_keep_engine_relevance_on_both_surfaces(self) -> None: + script = guard._display_path(guard._engine_script_path()) + command = f"python3 \"{script}\" scan --target '{{g}}' --output s" + self.assertTrue(guard._engine_gate_relevant(command, "Bash")) + self.assertTrue(guard._engine_gate_relevant(command, "PowerShell")) + self.assertFalse(guard._engine_gate_relevant("ls '{g}'", "Bash")) + self.assertFalse(guard._engine_gate_relevant("Get-Item '{g}'", "PowerShell")) + def test_operator_labels_cover_the_characters_the_literal_parser_rejects( self, ) -> None: @@ -15556,6 +15608,38 @@ def test_every_value_flag_carries_a_literal_it_admits(self) -> None: self.grammar.literal_value_ok(flag, cast(str, flag.example)) ) + def test_a_quoted_brace_target_is_classified_like_a_plain_one(self) -> None: + guid = "C:/Users/x/AppData/Local/wsl/{673ac4db-a2e3-459e-882c-1ec71b253aa2}" + for name in ("scan", "inventory"): + spec = self.grammar.subcommand(name) + plain = self.words(spec, optionals=False) + plain[plain.index("--target") + 1] = "target-dir" + braced = [guid if word == "target-dir" else word for word in plain] + command = self.command(name, braced) + single, double = command, command.replace(f"'{guid}'", f'"{guid}"') + self.assertIn(f"'{guid}'", single) + self.assertIn(f'"{guid}"', double) + expected = self.classify(name, plain) + self.assertEqual(name, expected) + for label, text in (("single", single), ("double", double)): + with self.subTest(subcommand=name, quote=label): + self.assertEqual( + expected, + guard.classify_exact_engine_command(text, self.AUTHORITY), + ) + + def test_an_unquoted_or_expanding_brace_target_is_refused(self) -> None: + spec = self.grammar.subcommand("scan") + words = self.words(spec, optionals=False) + head = self.command("scan", words) + for target in ("{a,b}", "x{a}", "'{a}'x", '"${x}"', '"$(x)"', "'{a}' '{b'"): + with self.subTest(target=target): + self.assertIsNone( + guard.classify_exact_engine_command( + f"{head} --policy {target}", self.AUTHORITY + ) + ) + def test_parser_declares_exactly_the_grammar_flags(self) -> None: subparsers = self.subparsers() self.assertEqual(list(self.grammar.SUBCOMMAND_NAMES), list(subparsers)) From 1276b8f49a9f8195cc97e549a79be53f254660c2 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:20:49 -0400 Subject: [PATCH 2/4] chore(disk-hygiene): release 0.41.3 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 1ea97e3365..325da77b68 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.41.2", + "version": "0.41.3", "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 ae2eb46f91..4276119e70 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.41.3] - 2026-09-30 + +### Fixed + +- **The engine gate accepts braces inside a quoted word** ([#5641](https://github.com/melodic-software/claude-code-plugins/issues/5641)). A `--target` such as `'C:/Users/me/AppData/Local/wsl/{673ac4db-a2e3-459e-882c-1ec71b253aa2}'` now forms an exact engine call, so the WSL distro folders can be snapshotted. `{` and `}` are literal inside a whole-word single or double quote and are accepted only there; every other expansion or operator character, `$` included, is still refused wherever it sits, and an unquoted brace is still refused. The denial no longer names a quoted brace as the culprit. + ## [0.41.2] - 2026-09-30 ### Changed From b09cd97c2c95912daa780ebd4d47989c9e263178 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:34:24 -0400 Subject: [PATCH 3/4] fix(disk-hygiene): satisfy markdown and machine-path lint for the quoted-brace change Co-Authored-By: Claude Opus 5.5 --- plugins/disk-hygiene/CHANGELOG.md | 3 ++- plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/disk-hygiene/CHANGELOG.md b/plugins/disk-hygiene/CHANGELOG.md index c0672cf856..57ccb295f8 100644 --- a/plugins/disk-hygiene/CHANGELOG.md +++ b/plugins/disk-hygiene/CHANGELOG.md @@ -7,7 +7,8 @@ All notable changes to the `disk-hygiene` plugin are documented here. Format fol ### Fixed -- **The engine gate accepts braces inside a quoted word** ([#5641](https://github.com/melodic-software/claude-code-plugins/issues/5641)). A `--target` such as `'C:/Users/me/AppData/Local/wsl/{673ac4db-a2e3-459e-882c-1ec71b253aa2}'` now forms an exact engine call, so the WSL distro folders can be snapshotted. `{` and `}` are literal inside a whole-word single or double quote and are accepted only there; every other expansion or operator character, `$` included, is still refused wherever it sits, and an unquoted brace is still refused. The denial no longer names a quoted brace as the culprit. +- **The engine gate accepts braces inside a quoted word** ([#5641](https://github.com/melodic-software/claude-code-plugins/issues/5641)). A `--target` such as `'D:/wsl/{673ac4db-a2e3-459e-882c-1ec71b253aa2}'` now forms an exact engine call, so the WSL distro folders can be snapshotted. `{` and `}` are literal inside a whole-word single or double quote and are accepted only there; every other expansion or operator character, `$` included, is still refused wherever it sits, and an unquoted brace is still refused. The denial no longer names a quoted brace as the culprit. + ## [0.41.3] - 2026-09-30 ### Changed diff --git a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py index a19e75620e..d0f1df0c50 100755 --- a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py +++ b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py @@ -15609,7 +15609,7 @@ def test_every_value_flag_carries_a_literal_it_admits(self) -> None: ) def test_a_quoted_brace_target_is_classified_like_a_plain_one(self) -> None: - guid = "C:/Users/x/AppData/Local/wsl/{673ac4db-a2e3-459e-882c-1ec71b253aa2}" + guid = "D:/wsl/{673ac4db-a2e3-459e-882c-1ec71b253aa2}" for name in ("scan", "inventory"): spec = self.grammar.subcommand(name) plain = self.words(spec, optionals=False) From e2f9b96c3687f99c7c1f57c38e631e46711fb019 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:47:35 -0400 Subject: [PATCH 4/4] fix(disk-hygiene): accept quoted braces in the absolute bracket-test belt The /usr/bin/[ ... ] read-only inspection scanned its interior for forbidden characters without the quoted-brace allowance, so a quoted GUID path was still denied there while the /usr/bin/test spelling accepted it. Co-Authored-By: Claude Opus 5.5 --- .../skills/clean/scripts/destructive_guard.py | 8 ++++++-- plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py b/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py index 9cd565e63c..361d213a9a 100755 --- a/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py +++ b/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py @@ -1445,7 +1445,9 @@ def _parse_bracket_test_words(command: str) -> list[str] | None: if len(text) < 2 or text[0] != "[" or text[-1] != "]": return None interior = text[1:-1] - if any(value in _SHELL_EXPANSION_OR_OPERATOR_CHARS for value in interior): + if _SHELL_EXPANSION_OR_OPERATOR_CHARS.intersection( + _without_quoted_braces(interior) + ): return None stripped = interior.strip() if not stripped: @@ -1673,7 +1675,9 @@ def _absolute_bracket_test_words(command: str) -> tuple[str, list[str]] | None: if rest != "]" and not rest.endswith(" ]"): return None interior = "" if rest == "]" else rest[:-1].strip() - if any(value in _SHELL_EXPANSION_OR_OPERATOR_CHARS for value in interior): + if _SHELL_EXPANSION_OR_OPERATOR_CHARS.intersection( + _without_quoted_braces(interior) + ): return None if not interior: return head, [] diff --git a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py index d0f1df0c50..389046f09d 100755 --- a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py +++ b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py @@ -11176,6 +11176,13 @@ def test_bracket_test_is_not_trusted_on_name_alone(self) -> None: self.assertFalse( guard.is_exact_readonly_supporting_command("/usr/bin/[ -d $(pwd) ]") ) + guid = "D:/wsl/{673ac4db-a2e3-459e-882c-1ec71b253aa2}" + self.assertTrue( + guard.is_exact_readonly_supporting_command(f"/usr/bin/[ -d '{guid}' ]") + ) + self.assertFalse( + guard.is_exact_readonly_supporting_command(f"/usr/bin/[ -d {guid} ]") + ) def test_classifier_rejects_a_subcommand_outside_the_shared_list(self) -> None: """The denial text and the grammar are one list, so they cannot drift."""