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.20.4",
"version": "0.20.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",
Expand Down
13 changes: 13 additions & 0 deletions plugins/disk-hygiene/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
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.20.5]

### Fixed

- **PowerShell `>>` append redirection is flagged like `>` (#2675).** `_POWERSHELL_OUTPUT_REDIRECT`
matched neither character of a `>>` pair — `(?![=>&])` rejects the first `>`, and the lookbehind
rejects the second — so `<cmd> >> append.txt` wrote a file with no prompt while the same command
with `>` prompted. Append is matched explicitly (`>>`, `2>>`, `*>>`) without widening that
lookahead (load-bearing for the stream-merge exclusion from #2627 and the `$null`-discard
exclusion from #2671). `>> $null` stays silent — a discard, not a file write — and requires a
real token terminator after `$null` so punctuation continuations like `>>$null/out.txt` stay
flagged.

## [0.20.4]

### Fixed
Expand Down
17 changes: 16 additions & 1 deletion plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -1014,9 +1014,22 @@ def is_exact_kill_switch_probe(command: str) -> bool:
# File redirects still match: `2>out.txt`, `> out.txt`, `1>file`, and a command
# that discards one stream while redirecting another (`... 2>$null > out.txt`),
# whose second `>` has no `$null` after it.
# Append (`>>`) is NOT covered here: `(?![=>&])` rejects the first `>` of the
# pair and the lookbehind rejects the second, so `>>` is invisible — see
# `_POWERSHELL_APPEND_REDIRECT` (#2675).
_POWERSHELL_OUTPUT_REDIRECT = re.compile(
r"(?i)(?<![<>])>(?![=>&])(?![^\S\n]*\$null(?=[\s;|)}]|$))"
)
# File append (`>>`, `2>>file`, `*>>file`). Matched separately rather than by
# widening the single-`>` lookahead, which is load-bearing for the stream-merge
# and `$null`-discard exclusions above. Exclude `>> $null` the same way
# guardrails' `ps::write_bypass` excludes the `$null` discard — case-
# insensitively, horizontal whitespace only, and with a real token terminator
# after `$null` (whitespace, `;`, `|`, `)`, `}`, or end-of-string) so
# punctuation continuations like `>>$null/out.txt` stay flagged.
_POWERSHELL_APPEND_REDIRECT = re.compile(
r"(?i)(?<![<>])>>(?![^\S\n]*\$null(?=[\s;|)}]|$))"
)
_POWERSHELL_DOTNET_DELETE = re.compile(r"(?i)(::\s*delete|\.\s*delete\s*\()")
# robocopy is an executable normally invocable by full path
# (C:\Windows\System32\robocopy.exe), so unlike the cmdlet word list its
Expand Down Expand Up @@ -1072,7 +1085,9 @@ def powershell_decision(command: str, enabled: bool) -> tuple[str, str] | None:
enabled,
"disk-hygiene flagged New-Item -Force (truncates an existing file).",
)
if _POWERSHELL_OUTPUT_REDIRECT.search(command):
if _POWERSHELL_OUTPUT_REDIRECT.search(
command
) or _POWERSHELL_APPEND_REDIRECT.search(command):
return _powershell_mutation_verdict(
enabled,
"disk-hygiene flagged shell output redirection (may overwrite a file).",
Expand Down
27 changes: 27 additions & 0 deletions plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py
Original file line number Diff line number Diff line change
Expand Up @@ -5604,6 +5604,33 @@ def test_powershell_null_discards_are_not_file_redirects(self) -> None:
):
self.assertIsNone(self.run_guard_powershell(command), command)

def test_powershell_append_redirects_are_file_writes(self) -> None:
"""#2675: `>>` appends to a file; it must prompt like `>`, not slip past."""
for command in (
"Get-ChildItem C:/tmp >> append.txt",
"Get-ChildItem C:/tmp 2>>err.txt",
"Get-ChildItem C:/tmp *>>all.txt",
# `$nullish` is an ordinary variable target, not the null device.
"Get-ChildItem C:/tmp >>$nullish",
# Punctuation after `$null` is a path continuation, not a discard.
"Get-ChildItem C:/tmp >>$null/out.txt",
"Get-ChildItem C:/tmp 2>>$null\\evil.ps1",
):
result = self.run_guard_powershell(command)
assert result is not None, command
self.assertEqual(
"ask",
result["hookSpecificOutput"]["permissionDecision"],
command,
)
for command in (
"Get-ChildItem C:/tmp >> $null",
"Get-ChildItem C:/tmp >>$null",
"Get-ChildItem C:/tmp 2>>$null",
"Get-ChildItem C:/tmp *>>$NULL",
):
self.assertIsNone(self.run_guard_powershell(command), command)

def test_powershell_bare_name_mentions_defer_but_engine_identity_denies(
self,
) -> None:
Expand Down