From 1502bd830d4d76df84c05cfd648c2aebf1676bdf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 14 Aug 2026 22:45:53 +0000 Subject: [PATCH] fix(disk-hygiene): do not flag PowerShell 2>&1 as file redirect Exclude stream-merge forms (`2>&1`, `*>&1`) from `_POWERSHELL_OUTPUT_REDIRECT` so ordinary stderr capture no longer prompts as a file-overwriting mutation (#2615). File redirects like `2>out.txt` still ask. Co-authored-by: Kyle Sexton --- plugins/disk-hygiene/.claude-plugin/plugin.json | 2 +- plugins/disk-hygiene/CHANGELOG.md | 13 +++++++++++++ .../skills/clean/scripts/destructive_guard.py | 4 +++- .../skills/clean/scripts/test_hygiene.py | 12 ++++++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/plugins/disk-hygiene/.claude-plugin/plugin.json b/plugins/disk-hygiene/.claude-plugin/plugin.json index 9fd44c30cf..cedfd71bb5 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.17.10", + "version": "0.17.11", "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, 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 4dcb28a963..58c4033b22 100644 --- a/plugins/disk-hygiene/CHANGELOG.md +++ b/plugins/disk-hygiene/CHANGELOG.md @@ -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.17.11] + +### Fixed + +- **PowerShell stream merges (`2>&1`, `*>&1`) are no longer flagged as file-overwriting + redirection (#2615).** `_POWERSHELL_OUTPUT_REDIRECT` matched the `>` inside `2>&1` because + its lookaround only excluded adjacent `<`, `>`, and `=`. In PowerShell `>&` is only ever a + stream merge and never designates a file, so ordinary diagnostic commands that capture + combined output were prompting as mutations — approval-fatigue noise that blunts real + deletion prompts, especially once the belt stays armed for the rest of the session (#2591). + The detector now also excludes a following `&`; `2>out.txt` and `'data' > file` still + prompt. + ## [0.17.10] ### Fixed diff --git a/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py b/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py index 6c73486d61..07f35864ea 100755 --- a/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py +++ b/plugins/disk-hygiene/skills/clean/scripts/destructive_guard.py @@ -968,7 +968,9 @@ def is_exact_kill_switch_probe(command: str) -> bool: _POWERSHELL_NEW_ITEM_FORCE = re.compile( r"(?i)(?])>(?![=>])") +# Exclude stream merges (`2>&1`, `1>&2`, `*>&1`): in PowerShell `>&` only merges +# streams and never designates a file. File redirects like `2>out.txt` still match. +_POWERSHELL_OUTPUT_REDIRECT = re.compile(r"(?])>(?![=>&])") _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 diff --git a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py index fd39ac0141..73ac4f24d6 100755 --- a/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py +++ b/plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py @@ -4574,6 +4574,7 @@ def test_powershell_deletion_spellings_force_final_prompt(self) -> None: "Out-File C:/tmp/file.txt -Force", "New-Item C:/tmp/file.txt -ItemType File -Force", "'data' > C:/tmp/file.txt", + "Get-ChildItem C:/tmp 2>out.txt", ): result = self.run_guard_powershell(command) assert result is not None, command @@ -4583,6 +4584,17 @@ def test_powershell_deletion_spellings_force_final_prompt(self) -> None: command, ) + def test_powershell_stream_merges_are_not_file_redirects(self) -> None: + """#2615: `2>&1` / `*>&1` merge streams; they must not prompt as file redirects.""" + for command in ( + "bash plugins/disk-hygiene/skills/clean/scripts/hygiene.test.sh 2>&1 | Select-Object -Last 8", + "git status --short 2>&1", + "Get-ChildItem C:/tmp 1>&2", + "Get-ChildItem C:/tmp *>&1", + "Get-ChildItem C:/tmp 2>&1 | Select-Object -First 1", + ): + self.assertIsNone(self.run_guard_powershell(command), command) + def test_powershell_bare_name_mentions_defer_but_engine_identity_denies(self) -> None: """F6 (#1112): mentions defer via the invocation classifier; a command whose argument IS the bundled engine still denies — verb names prove