Skip to content

fix(disk-hygiene): do not flag PowerShell 2>&1 as file redirect - #2627

Merged
kyle-sexton merged 1 commit into
mainfrom
cursor/fix-disk-hygiene-ps-stream-merge-f8b0
Aug 14, 2026
Merged

fix(disk-hygiene): do not flag PowerShell 2>&1 as file redirect#2627
kyle-sexton merged 1 commit into
mainfrom
cursor/fix-disk-hygiene-ps-stream-merge-f8b0

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Closes #2615

Summary

The disk-hygiene PowerShell belt treated 2>&1 stream merges as file-overwriting redirects because _POWERSHELL_OUTPUT_REDIRECT matched the > in 2>&1.

Fix

Exclude PowerShell stream-merge redirections (N>&M) from the file-overwrite detector. Add regression coverage.

Verification

See branch CI / local destructive_guard / hygiene tests on the PR checks.

Related

Refs #2589 — argument-surface docs (serial disk-hygiene lane).
Refs #2591 / #2618 — belt lifetime (later wave).

Open in Web Open in Cursor 

@cursor

cursor Bot commented Aug 14, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 27s —— View job


Security review — no issues found

  • Gather context (PR diff, changed files)
  • Review changed files for security-relevant issues
  • Report findings or confirm no issues

This PR changes the regex _POWERSHELL_OUTPUT_REDIRECT in destructive_guard.py, a guard used to require human confirmation before running potentially destructive PowerShell commands. Since this is safety/guard logic, I specifically checked whether the fix could weaken detection and allow a genuinely destructive command to slip through unprompted.

Old: (?<![<>])>(?![=>]) — flags any > not adjacent to <, >, or =.
New: (?<![<>])>(?![=>&]) — additionally excludes a > immediately followed by &.

Analysis:

  • In PowerShell, n>&m (e.g. 2>&1, *>&1) is exclusively stream-merge syntax — it merges one stream into another's existing destination and can never itself designate a file target. So excluding it from the "may overwrite a file" heuristic is semantically correct, not a weakening.
  • A composed idiom like 1>file.txt 2>&1 still trips the guard, because the 1>file.txt portion is a > followed by f, not &, so it still matches. The new exclusion only ever suppresses the specific >& token, never a real file redirect standing next to it.
  • A malformed spelling like >&x (not a digit/*) isn't valid PowerShell redirection syntax at all and would fail to parse/execute rather than silently writing a file, so there's no practical bypass there.
  • Out-File, Set-Content, Add-Content, etc. remain caught independently by _POWERSHELL_MUTATION_WORDS, and 2>out.txt (a real file redirect) is covered by the new regression test and still correctly flagged.

No injection, authorization, secrets-handling, or CI/workflow-permission issues in this changeset — it's a self-contained Python regex fix plus a test and changelog/version bump. No security concerns to report.

@kyle-sexton
kyle-sexton enabled auto-merge (squash) August 14, 2026 22:49
@github-actions

Copy link
Copy Markdown

Last security-reviewed head: 45d24d59b156b47aa4a11510d528e1252cdc55b7. On the next push, the relevance gate compares only the commits since this SHA; delete this comment to force a full re-review.

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 <kyle-sexton@users.noreply.github.com>
@cursor
cursor Bot force-pushed the cursor/fix-disk-hygiene-ps-stream-merge-f8b0 branch from 45d24d5 to 1502bd8 Compare August 14, 2026 22:57
@kyle-sexton
kyle-sexton merged commit 086253b into main Aug 14, 2026
39 checks passed
@kyle-sexton
kyle-sexton deleted the cursor/fix-disk-hygiene-ps-stream-merge-f8b0 branch August 14, 2026 23:04
@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cursor[bot]'s task in 26s —— View job


Code review — skipped

  • Gather context (PR diff, changed files, PR state)
  • Apply skip gate

Skip gate: this PR is already MERGED (not open), which matches skip-gate condition 1 ("PR is closed or not open") in the /review:code-review lane. Per that gate, no further review is performed and nothing else is posted.

For reference, this PR was already reviewed for security by the /review:security-review lane (see the earlier comment above, "Security review — no issues found").

@github-actions

Copy link
Copy Markdown

Claude has reviewed this PR 1 time. The lane skips further automatic reviews after 5; deleting this comment resets the count.

kyle-sexton added a commit that referenced this pull request Aug 15, 2026
…direction (#2671)

## Problem

PR #2627 closed issue #2615 by excluding stream merges (`2>&1`, `1>&2`,
`*>&1`) from
`_POWERSHELL_OUTPUT_REDIRECT`. That fix is correct but partial: it only
excludes `&`.

In a **discard** the character after `>` is `$`, not `&`:

```python
# on origin/main
_POWERSHELL_OUTPUT_REDIRECT = re.compile(r"(?<![<>])>(?![=>&])")
```

So `2>$null` — PowerShell's `/dev/null`, and the standard way to silence
a noisy read-only
command — still matches, and the guard still prompts with *"disk-hygiene
flagged shell output
redirection (may overwrite a file)"*. The issue closed while the
operator's actual failing
commands kept prompting.

## Observed cost

Two read-only commands from an attended session, both prompted:

- `gh issue list --repo <owner/repo> --state open 2>$null`
- `chezmoi managed | Select-String -Pattern claude 2>$null`

Neither can write a file. Each prompt stalled the session. This is the
approval-fatigue
mechanism issue #2615 itself describes: a destructive-action guard that
cries wolf on read-only
work measurably degrades the signal of its genuine prompts.

## Fix

```python
_POWERSHELL_OUTPUT_REDIRECT = re.compile(
    r"(?i)(?<![<>])>(?![=>&])(?![^\S\n]*\$null(?![\w-]))"
)
```

Three deliberate details:

- **`(?i)`** — PowerShell variable names are case-insensitive, so
`2>$NULL` is the same discard.
- **`[^\S\n]*`** — only *horizontal* whitespace is skipped, so a
trailing `>` at end of line
  cannot borrow a `$null` from the next line.
- **`(?![\w-])`** — `$nullish` and `$null-backup` are ordinary
variables, not the null device,
  and must still be treated as file targets.

The exclusion is spelled the way guardrails' `ps::write_bypass` already
spells the same
`$null` exclusion, rather than inventing a second spelling for the same
concept in the same
fleet.

## Coverage

Allowed (newly, 9 forms): `2>$null`, `*>$null`, `>$null`, `2> $null`,
`2>$NULL`, `2>$null;
<cmd>`, `2>$null | <cmd>`, and the two real-world commands above.

Still flagged (7 forms): `2>out.txt`, `> out.txt`, `1>file`,
`2>$nullish`, `2>$null-backup`,
`2>$null > out.txt`, `2>&1 > out.txt` — the last two being the important
ones: discarding one
stream while redirecting another is still a file write, and the second
`>` has no `$null` after
it.

## Test evidence

`test_powershell_null_discards_are_not_file_redirects` covers every form
above.

Full-suite comparison, same interpreter, same machine:

| tree | tests | result |
|---|---|---|
| `origin/main` (pristine, extracted via `git archive`) | 282 | 3
failures, 4 skipped |
| this branch | 283 | **the same 3 failures**, 4 skipped |

The three pre-existing failures are
`test_deny_emits_blocked_telemetry_when_sink_wired`,
`test_preview_allows_root_children_os_managed_snapshot`, and
`test_stash_must_exist_in_an_independent_checkout`. They are identical
on both trees and
unrelated to this change; this branch adds one passing test and
introduces no new failure.

## Adjacent gap found, deliberately NOT fixed here

`>>` (append) is matched by **neither** the old nor the new pattern —
`(?![=>&])` rejects the
first `>` of the pair, and the cmdlet word list catches
`out-file`/`add-content` but not a bare
`>>` redirect. So `<cmd> >> append.txt` writes a file without a prompt.

That is pre-existing on `main` and orthogonal to this change, so it is
reported rather than
folded in. Worth its own issue.

## Related

- #2627 — the stream-merge fix this completes; it closed #2615 for the
`>&` form only.
- #2674 — concurrent PR against the same plugin; it claims version
`0.20.3`, this one `0.20.2`.
- The `>>` append gap described above is unfiled and needs its own
issue.

Closes #2615

---------

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
kyle-sexton added a commit that referenced this pull request Aug 15, 2026
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
Closes #2675

## Summary

`_POWERSHELL_OUTPUT_REDIRECT` matched neither character of a `>>` pair,
so `<cmd> >> append.txt` wrote a file with no prompt while the same
command with `>` prompted.

## Fix

- Explicit append matcher (`>>` / `2>>` / `*>>`) without widening
stream-merge / `$null`-discard behavior
- `>> $null` stays silent, requiring a real token terminator after
`$null` (same boundary as #2671)
- Punctuation continuations like `>>$null/out.txt` stay flagged
- Plugin version `0.20.5` (above #2671's `0.20.4`)

## Verification

Focused GuardTests for append redirects and stream merges pass locally.

## Related

Refs #2615 / #2627 / #2671 — stream-merge and `$null`-discard carve-outs
that must stay narrow.

<!-- CURSOR_AGENT_PR_BODY_END -->

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

disk-hygiene: the PowerShell belt flags 2>&1 stream merges as file-overwriting redirection

2 participants