Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/disk-hygiene/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "disk-hygiene",
"version": "0.42.0",
"version": "0.42.1",
"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",
Expand Down
6 changes: 6 additions & 0 deletions plugins/disk-hygiene/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.42.1] - 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 `'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.42.0] - 2026-09-30

### Added
Expand Down
26 changes: 21 additions & 5 deletions plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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>['\"])[^'\"]*(?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]:
Expand Down Expand Up @@ -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] = []
Expand Down Expand Up @@ -1433,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:
Expand Down Expand Up @@ -1661,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, []
Expand Down Expand Up @@ -2178,7 +2194,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
Expand Down
91 changes: 91 additions & 0 deletions plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -11124,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."""
Expand Down Expand Up @@ -15556,6 +15615,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 = "D:/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))
Expand Down
Loading