Skip to content

fix(disk-hygiene): restore Windows readonly allowlist; ask in engine-gate - #2786

Merged
kyle-sexton merged 3 commits into
mainfrom
cursor/2774-win-allowlist-8f91
Aug 15, 2026
Merged

fix(disk-hygiene): restore Windows readonly allowlist; ask in engine-gate#2786
kyle-sexton merged 3 commits into
mainfrom
cursor/2774-win-allowlist-8f91

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Test plan

  • python -m unittest test_hygiene.GuardTests (126 tests OK on Linux)
  • New cases: MSYS-before-absolute, basename strip, LocalAppData exclusion, engine-gate ask

Closes #2774

Related

…gate

Move MSYS path mapping ahead of is_absolute(), strip .exe basenames,
downgrade engine-gate readonly decisions to ask, exclude LocalAppData
trust roots, and add a Windows GuardTests CI lane.

Closes #2774

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@cursor

cursor Bot commented Aug 15, 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 15, 2026

Copy link
Copy Markdown
Contributor

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


Security review — /review:security-review

  • Gather context (PR diff, changed files, custom instructions)
  • Review .github/workflows/ci.yml changes (Actions security)
  • Review destructive_guard.py changes (allowlist / trust logic)
  • Review test_hygiene.py changes
  • Post findings

No security issues found. This is a well-scoped security fix PR and I did not find any new vulnerabilities it introduces.

Reviewed with focus on logic flaws, trust-boundary reasoning, and GitHub Actions security (per /review:security-review criteria — zizmor's static-analysis lane already covers supply-chain/unpinned-action/permission findings, so those aren't re-litigated here).

What the diff actually does, security-wise:

  • destructive_guard.py L1196-1238: moves the MSYS-path reinterpretation ahead of Path.is_absolute(). I checked whether this makes previously-dead code newly reachable in a way that weakens the allowlist — it does (the MSYS branch was unreachable before), but the final gate (_path_under_trusted_readonly_bin, L1182) still requires the symlink-resolved absolute path to be anchored under a small set of trusted directories, so ..-laden rel_parts can't escape the allowlist even though they aren't explicitly normalized. Not exploitable as far as I can tell.
  • destructive_guard.py L1103-1135: removes the user-writable %LOCALAPPDATA%\Programs\Git trust root. This is a genuine hardening — a local, non-admin plant under that path could previously have contributed an allowlisted binary path; that gap is now closed.
  • destructive_guard.py L1685-1703: downgrades the readonly-allowlist decision from hard allow to ask specifically in engine-gate mode, closing a real permission-prompt bypass for consumer sessions. Correctly scoped — belt mode (used by the plugin's own bundled invocations) keeps hard allow.
  • .github/workflows/ci.yml: new disk-hygiene-guard-windows job uses SHA-pinned actions, persist-credentials: false, permissions: contents: read (inherited, unchanged), no secrets, and only runs the test suite. No Actions-security concerns.
  • test_hygiene.py: test-only additions exercising the new behavior; no production impact.

No CRITICAL, IMPORTANT, or SUGGESTION-level findings to report.

@github-actions

Copy link
Copy Markdown

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

…etry

Quote or prefer MSYS heads when which() returns a Program Files path, and use
a .cmd Python sink on NT so HOOK_TELEMETRY_SINK can exec under CreateProcess.

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cursor[bot]'s task in 2m 53s —— View job


Code review — /review:code-review

  • Gather context (PR diff vs. base 2fb7f7b, 5 changed files, empty CLAUDE.md — no repo-specific conventions to layer in)
  • Review destructive_guard.py (MSYS reorder, basename strip, LocalAppData exclusion, engine-gate ask downgrade)
  • Review test_hygiene.py, ci.yml, plugin.json/CHANGELOG.md
  • Post findings

Summary: This is a tight, well-targeted fix. The core logic changes hold up:

  • _readonly_supporting_basename (destructive_guard.py:1059-1072) only strips .exe/.com/.bat/.cmd under os.name == "nt", and the allowlisted basenames (ls, find, stat, …) contain no dots, so the extension-stripping introduces no false-positive risk against the current allowlist.
  • _trusted_system_readonly_head's reorder (destructive_guard.py:1196-1238) correctly moves the MSYS branch ahead of Path.is_absolute() (which is False for POSIX-style heads on Windows-native Python) — this genuinely fixes the "inert allowlist" defect, and the final gate (_path_under_trusted_readonly_bin) still anchors on the resolved root, so nothing in the reorder weakens the trust boundary.
  • Excluding %LOCALAPPDATA%\Programs\Git from _nt_known_git_installation_roots (destructive_guard.py:1103-1135) is a correct hardening — that path is user-writable, so it shouldn't have been a trust root.
  • The _decide engine-gate downgrade to ask (destructive_guard.py:1685-1703) is correctly scoped: only resolve_mode() == _MODE_ENGINE_GATE is affected, belt mode is untouched, and it's the only call site for is_exact_readonly_supporting_command's decision.
  • New tests (test_msys_posix_head_is_mapped_before_is_absolute_gate, test_readonly_supporting_basename_strips_windows_extensions, test_nt_git_roots_exclude_user_writable_localappdata, test_engine_gate_asks_not_allows_readonly_supporting_commands) exercise exactly the previously-dead/regressed branches, and the Windows telemetry-sink .cmd rework is a sensible fix for CreateProcess not exec'ing #!/bin/sh.
  • CHANGELOG (0.20.8) and plugin.json version bump are consistent with the diff.

One maintainability nit (not on a changed line, so noted here rather than inline):

  • .github/workflows/ci.yml#L273 — the comment above the hook-utils-windows job still says "The ONLY job on a Windows runner, and deliberately so." This PR adds a second Windows job, disk-hygiene-guard-windows (L319), right after it, so that claim is now stale and could mislead whoever reads it before adding a third lane. Worth a quick reword (e.g. drop "ONLY" / merge the shared "keep Windows lanes small" rationale) — non-blocking.

    Fix this →

No correctness bugs found. (Note: an inline comment attempt on ci.yml:273 failed — GitHub rejects anchoring outside the diff's hunk context, since that line itself is unchanged — so it's captured here instead, per the review instructions for cross-diff findings.)

Use a .cmd→.py sink and wait for non-empty content so open('w') cannot
race the JSON parse on the fire-and-forget HOOK_TELEMETRY_SINK path.

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

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

@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cursor[bot]'s task in 4m 45s —— View job


Code review — /review:code-review

  • Gather context (PR diff, changed files, custom instructions)
  • Review destructive_guard.py changes (MSYS mapping, allowlist, engine-gate ask)
  • Review test_hygiene.py changes
  • Review .github/workflows/ci.yml changes (new Windows lane)
  • Review CHANGELOG / plugin.json version bump
  • Post findings

Scope note: a claude-security-review lane already ran on this PR and reported no issues, so per REVIEW.md this pass is scoped to correctness/design/tests/docs and does not re-litigate the trust-boundary reasoning it already covered (MSYS mapping order, ..-laden rel_parts anchoring, LocalAppData exclusion, engine-gate ask scoping — all independently spot-checked here too, no disagreement).

Findings

1 (Suggestion, inline)plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py:4551-4561: the LocalAppData fixture added to test_nt_git_roots_ignore_path_selected_git doesn't verify what its comment claims (the planted directory doesn't align with the join logic the removed candidate tuple used). Not a coverage gap — the dedicated test_nt_git_roots_exclude_user_writable_localappdata right below it covers the regression correctly — but worth tightening so it doesn't mislead a future reader. Posted inline.

2 (Suggestion).github/workflows/ci.yml#L273-L288: the block comment above hook-utils-windows states "The ONLY job on a Windows runner, and deliberately so" and explains why every other suite is Linux-safe. This PR adds a second windows-2025 job (disk-hygiene-guard-windows, L319-L336) immediately below it, so the claim is now false in the same file this PR touches. Not anchorable inline (the comment line itself is outside the diff hunk), but worth a follow-up tweak — e.g. drop "the ONLY job" framing or note the disk-hygiene exception — so the comment doesn't actively mislead the next person reasoning about which suites need Windows coverage.

What else was checked (no issues)

  • _readonly_supporting_basename / .exe/.com/.bat/.cmd stripping: correctly gated to os.name == "nt", off-Windows behavior unchanged, matches new test coverage.
  • MSYS-before-is_absolute() reordering: traced the rel_partsgit_root.joinpath(...).resolve()_is_within_directory anchored-prefix check; resolve() collapses any .. before the anchor check runs, so no traversal bypass.
  • Engine-gate ask downgrade is correctly scoped to _MODE_ENGINE_GATE only; belt mode (is_exact_readonly_supporting_command reached via the plugin's own bundled invocations) keeps hard allow, matching the new test_engine_gate_asks_not_allows_readonly_supporting_commands coverage.
  • plugin.json version bump (0.20.7 → 0.20.8) and CHANGELOG entry accurately describe the diff; no other version references needed updating.
  • CI job: SHA-pinned actions consistent with sibling jobs, python-version: '3.14' and the setup-python pin match the other lanes exactly, and disk-hygiene-guard-windows was correctly added to the ci-status needs: list so it's an actual required gate, not just informational.

No blocking (Critical/Important) findings.

@kyle-sexton
kyle-sexton merged commit 213bdce into main Aug 15, 2026
43 checks passed
@kyle-sexton
kyle-sexton deleted the cursor/2774-win-allowlist-8f91 branch August 15, 2026 23:48
Comment on lines 4551 to +4561
{
"ProgramFiles": str(base / "empty-pf"),
"ProgramFiles(x86)": str(base / "empty-pf86"),
"LocalAppData": str(base / "empty-local"),
"LocalAppData": str(base / "Programs" / "Git"),
"SystemRoot": "",
"SYSTEMROOT": "",
},
clear=False,
),
):
# Even with a user-writable LocalAppData\\Programs\\Git tree

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this fixture doesn't actually verify the LocalAppData exclusion it claims to. It sets LocalAppData = base/"Programs"/"Git" and creates that exact directory, but the (now-removed) candidate tuple was ("LocalAppData", ("Programs", "Git")), i.e. the code would have joined "Programs"/"Git" onto the env value — so the path it would have probed is base/Programs/Git/Programs/Git, not the base/Programs/Git planted here. Even if that candidate were ever accidentally reintroduced, this particular fixture wouldn't catch it (the directory the reintroduced code would check still wouldn't exist).

test_nt_git_roots_exclude_user_writable_localappdata right below (lines 4569-4591) does align the planted directory correctly (LocalAppData = base/"Local", planted at base/"Local"/"Programs"/"Git") and is the one that actually guards this regression — so this isn't a coverage gap, just a misleading comment/assertion pairing here that could confuse a future reader into thinking this test proves something it doesn't.

Fix this →

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: read-only Bash allowlist is inert on Windows (0 commands accepted); engine-gate hard-allow leak on Linux

2 participants