Skip to content

fix(guardrails): PowerShell here-string / bare-CR shapes hiding git (#4683) - #5036

Merged
cursor[bot] merged 5 commits into
mainfrom
cursor/4683-ps-opener-bare-cr-37e9
Sep 28, 2026
Merged

cursor[bot] merged 5 commits into
mainfrom
cursor/4683-ps-opener-bare-cr-37e9

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Closes #4683.

Supersedes closed draft #4978 (stale tip on cursor/4683-ps-herestring-bare-cr-37e9).

Summary

Refuse PowerShell here-string opener-untrusted, comment-span, and bare-CR shapes that hide git from the dangerous-git / no-verify / convention / noncanonical matchers. Match JSON \r for bare CR; keep verbatim here-string git as data; refuse N2 orphan closers; retarget #4682 budget pins.

Claim

These PowerShell shapes must refuse when they would otherwise hide git mutation from the guardrails matchers.

Basis

#4683 reproduction shapes; Windows PowerShell here-string / CR semantics; in-repo matcher differential vs origin/main (no under-block). Real matcher coverage, not a docs park.

As of

2026-09-28

Recheck

New PS hide shapes appear in the differential corpus, or upstream PowerShell quoting changes the opener grammar.

Test plan

Open in Web Open in Cursor 

cursoragent and others added 4 commits September 28, 2026 04:58
…ent-span, and bare CR

Generalize the no-token herestring-comment-char flag to a reduction-untrusted
family so a quote/backslash/backtick on a confirmed opener prefix, a <# earlier
in the command, or a CR that is not part of CRLF cannot drop live git or write
lines as here-string body. classify returns 2 when the flag is up even if
another sink trigger already fired, so a { plus commented opener reaches the
FLAG-keyed readers. No new allow tokens.

Closes #4683

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
…ing git as data

payload_has_bare_cr now glob-matches the two-character JSON \r escape (a
single-quoted \\r pattern was two backslashes and missed the encoding
hook::jq_fields leaves on INPUT). Git-freedom after a trusted opener still
runs on PS_BLANKED, so a verbatim body that merely names git stays data.

Closes #4683

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
A column-zero '@ / "@ with no confirmed opener hid git behind a quote the
Bash tokenizer never closed. Trailing CR is chomped per here-string line so a
CRLF canonical commit still classifies as the LF form. Guardrails 0.40.2.

Closes #4683

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
The trailing-space here-string opener is unconfirmed, so `"@` at column
zero is herestring-orphan-closer. Pin the budget-exhausted message on
the quoted-call no-progress shape, and expect the git-free payload to
refuse as that closer.

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

Copy link
Copy Markdown
Contributor

PR body contract — issue linkage

This PR body does not yet satisfy the issue-linkage contract:

  • Missing a "## Fix" section. State the concrete change and how it addresses the problem.
  • Missing a "## Verification" section. Record concrete evidence the change works (commands, gates, output).
  • Missing a "## Related" section. List related PRs, ADRs, or decision-log entries this PR does not close.

Edit the body and this comment updates itself on the next run.

…reset

Shellcheck SC2034 flags the cross-file reset of PS_HERESTRING_OPENER_COMMENT_CHAR.
The portability gate flags fixture strings that name grep or sort.

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@cursor
cursor Bot marked this pull request as ready for review September 28, 2026 06:21
@cursor
cursor Bot merged commit 0e2b4bd into main Sep 28, 2026
32 checks passed
@cursor
cursor Bot deleted the cursor/4683-ps-opener-bare-cr-37e9 branch September 28, 2026 06:33
kyle-sexton added a commit that referenced this pull request Sep 30, 2026
…5340)

No related issue: audit follow-up for the guardrails plugin. Every issue
it touches is already closed and several are being reopened by hand, so
no closing keyword is used.

## Summary

An audit of the 499 PRs an unattended agent opened between 2026-09-27
and 2026-09-29 found guardrails defects in six groups: guard gaps that
let a destructive command through, tests that no longer test what they
claim, a non-atomic cache, an undeclared Node requirement, README and
CHANGELOG statements that were wrong, and an unratified performance
claim in PLAN.md. This PR fixes what does not need the owner, and leaves
the owner-reserved questions alone (see Related). Guardrails goes 0.41.9
to 0.42.0.

## Fix

- `block-windows-drive-tmp`: a drive-root `\tmp` is refused on a
usertemp host; `curl`/`wget` short-flag clusters, `--output-dir` and an
option after a bare `-O` are judged; the usertemp mount fallback needs
`usertemp` on the `/tmp` mount's own line. The suite feeds commands on
stdin, so the 9 `/usr/bin` writer cases that were skipped now run.
- `block-root-delete-target` (PowerShell): a delete after LF, CRLF or a
bare CR, or inside a scriptblock, grouping or `$( )`, is judged;
`$env:NAME\subpath` no longer refused as a bare variable (`$env:TEMP`,
`$env:TEMP\`, `$env:TEMP\*` still are).
- PowerShell classifier: one untrusted-reduction flag instead of two, CR
scan skipped when the command has no CR, sink-budget tests rebuilt so
they exercise budget exhaustion.
- `stale-path-verify` and `skill-reference-verify`: a partly written
cache is a miss and is rebuilt; no extra process on the hook path.
- `wsl` regression rows added to the `block-hook-bypass`,
`block-noncanonical-commit` and `block-convention-violation` suites.
- Node on PATH is declared in the README, checked by `/guardrails:setup
check`, and listed in `prerequisites.json`; setup's apply text follows
the reconfiguration convention.
- README: `wsl` since-version, six re-parsing guards, PowerShell
no-token over-blocks, plugin-scoped GitHub MCP matcher, contributor
to-do and tracker asides removed. PLAN.md: the "no further process can
be removed" floor is scoped to the PostToolUse cold path and the goal is
marked a draft.
- CHANGELOG: five released entries corrected in place (0.41.7, 0.40.0,
0.38.11, 0.38.1, 0.37.3), no heading removed. They are named in the new
0.42.0 entry.
- From other groups' requests (extending the 0.42.0 entry, no second
bump):
- README: the exec-form dispatcher rows are dated to 0.41.3 (they said
0.41.0), and each fire is stated as two processes, node and then the
bash it spawns, with the candidate order left to the header of
`hooks/exec-bash.mjs` (hook-launcher).
- `/guardrails:setup check` probes the bash `hooks/exec-bash.mjs`
resolves, not the Bash tool's own bash (hook-launcher).
- PLAN.md says its census rows and floor line were measured under shell
form and not re-measured under exec form, and that no hook or skill
reads it (hook-launcher).
- `exec-bash.test.sh` accepts the first bash on `PATH` (a Homebrew bash)
as well as `/bin/bash` and `/usr/bin/bash`, and the plugin's resolver
test gains the PATH-first cases from `lib/exec-bash.resolver.test.mjs`
(hook-launcher).

## Verification

- `git merge origin/main`: two conflicts, the plugin version and the
CHANGELOG head, resolved by keeping 0.42.0 above the 0.41.9 entry that
#5309 added on main.
- `scripts/check-changelog-parity.sh --check --check-order`: pass.
`--check-preserved origin/main`: all 225 headings preserved.
- `scripts/validate-plugins.sh`: all manifests and the catalog validate.
- `scripts/affected-tests.sh --run --jobs 8`: every guardrails suite
passes (block-windows-drive-tmp, block-root-delete-target,
block-dangerous-git, block-hook-bypass, block-noncanonical-commit,
run-guards, stale-path-verify, skill-reference-verify, setup and the
rest). Three suites fail on this host for missing tools, unrelated to
the change: `scripts/check-html-assets.test.sh` and
`scripts/check-script-contract.test.sh` (htmlhint not installed),
`scripts/hook-census.test.sh` (strace not installed). 25 Node and Python
suites are selected but belong to other lanes.
- `scripts/check-guardrails-ps-differential.sh origin/main`: 613
commands, 0 cells where main refuses and this branch does not. The
scratch under-block differentials for block-windows-drive-tmp (6648
cells, 0 lost) and block-root-delete-target (15823 payloads; the 31
cells that differ are `$env:NAME\subpath` spellings Bash also allows,
plus one backtick-before-CR-CR-LF read as PowerShell reads it) are
recorded in the task notes.
- Baseline differential runs at the two earlier commits and at main: 0
cells lost in all three. Substitution-cap cost on WSL2 Linux: 2000
unquoted `$(:)` took a median 6038 ms; the same 2000 inside single
quotes or a heredoc body took 140 ms, level with a 137 ms control with
no substitution text. No Windows measurement was taken.
- No Windows host and no strace here: Windows behavior is proven by the
test-windows lane after the ready flip, and the spawn ceilings by CI's
ratchet step.
- Cross-group pass, on head `c3111b8d6` after merging origin/main
`0589e13f4` (clean):
- `exec-bash.test.sh` and `exec-bash.resolver.test.sh` pass. With a bash
symlink first on `PATH` outside `/bin` and `/usr/bin`, the old pin fails
(`FAIL: bash was not a real path`) and the new one passes.
- `check-changelog-parity.sh --check`, `--check-order`,
`--check-preserved origin/main` (226 headings) and `--check-bump
origin/main` pass.
- `validate-plugins.sh`, `sync-exec-bash.sh --check` (21 copies) and
`check-skill.sh` on the setup skill (PASS, 0 errors) pass. So do
`coverage-manifest.test.sh` (17 checks), `check-skill-portability.sh`
and `check-shell-portability.sh`. markdownlint, typos and shellcheck are
clean on the changed files.
- `scripts/affected-tests.sh --run --jobs 8` against origin/main: 287
selected suites pass. The same three as before fail on this host for
missing tools: `check-html-assets.test.sh` and
`check-script-contract.test.sh` (htmlhint), and `hook-census.test.sh`
(strace).
- PR #4715's disclosed "`git reset --hard` + nested here-string" bypass:
12 PowerShell nested here-string payloads carrying `git reset --hard`
exit 2 from `block-dangerous-git.sh` on this branch and on main, so no
issue was filed. One Bash probe exits 0 on both: a `-c` operand built
from a command substitution, which is the README's declared `$(…)`
residual.
- Ready pass, on head `600ec5b3c` after merging origin/main `a82943beb`
(clean; main's 7 new commits touch no guardrails path):
- Security review of the PR diff: no findings. Every guard change adds
refusals or matches the Bash lane. Probes still refuse `$env:TEMP\..`,
`${env:TEMP}\.`, a stray `)` or `}` before a delete, a delete after a
keyword block, unclosed levels, and a delete after a JSON `\r` or
`\u000d`. A raw control byte in the payload is invalid JSON, so the CR
re-read keeps the command as first read.
- `check-changelog-parity.sh` (`--check`, `--check-order`,
`--check-preserved origin/main`, `--check-bump origin/main`),
`validate-plugins.sh`, `sync-exec-bash.sh --check`, shellcheck,
markdownlint and typos on the changed files: pass. `exec-bash.test.sh`
and the node resolver test pass.
- `scripts/affected-tests.sh --run --jobs 8 origin/main`: the same three
suites fail for missing tools (htmlhint, strace).
`audit-coverage.test.sh` and `cant-fail-scan.test.sh` also failed in the
8-job run; both pass run alone on this head and on a snapshot of
`a82943beb`, and this PR does not touch their paths.

## Related

Audit: `.work/audit/REPORT.md` (local, not committed). Refs #3683 #3686
#3951 #4118 #4236 #4242 #4247 #4251 #4261 #4390 #4516 #4527 #4528 #4651
#4678 #4679 #4681 #4682 #4683 #4685.

Left for the owner, not implemented here:
- #4390: reopened with a decision packet on the cache design, plus an
addendum on PLAN.md's Done-when (ratify the ceilings, k × S, or
re-measure on Windows first) and `async` keep-or-reverse.
- #4684: reopened with a decision packet on the substitution cap against
the hook-precision rule.
- #5341: a new decision issue for block-root-delete-target scope
(launcher grammar, second PowerShell parser).
- #4118, #4681 and #4683: reopened with ratify-or-reverse packets,
because #4766, #4755 and #5036 took decisions reserved for the owner and
were merged by `app/cursor`. #3683 gets the same packet for #4845's
host-skip call.
- #4679: a packet on whether hook-observability condition 3 admits a
once-per-(session, agent) latch, or the admission is recorded as an
owner-approved exception.
- #3951: a packet on whether to build the heredoc-aware fix now in its
own PR, since the owner's 2026-09-28 comment closing #4811 said more
guard logic is not justified now.

Operator-only steps stay open on #3683, #4236, #4261, #4527, #4678,
#4679 and #4684 (Windows-host measurements and runs, and one interactive
render check).

Found and declared, not fixed: `env -f x rm -rf /` exits 0; PowerShell
`$r = Remove-Item ...`, comma-list and here-string forms remain gaps in
block-root-delete-target. An unmeasured Windows `RUN_GUARDS_PROFILE`
cost stays on #4236.

Cross-group requests:
- claude-ops: `audit_skill_visibility.test.sh` skips its whole
`--installed` contract on any Git Bash host (apply `host_path` to the
`--installed` argument or narrow the probe; correct the CHANGELOG
guess).
- claude-config: `unhobble/SKILL.md` (~L382) and
`unhobble/evals/evals.json` restate the block-hook-bypass exit-code
claim with no verification record.
- planning: `planning/skills/setup/SKILL.md` (~L158) has an unwrapped
line in the apply bullet.
- conventions: `docs/conventions/hook-observability/README.md` condition
3 needs to say whether a once-per-(session, agent) latch counts as a
state transition.
- scripts: add the shapes from the PowerShell differential findings to
`scripts/guardrails-ps-differential-corpus.jsonl`.
- hook-launcher: #5309 (0.41.9) landed the PATH lookup and the visible
notice for an unresolvable bash in `exec-bash.mjs`. Still open: a
missing node needs a launcher design that does not depend on node
(#3708). The README wording and the PLAN.md census note are now in this
PR.
- scripts: the 12 nested here-string `git reset --hard` payloads above
are candidates for the same corpus.

Cross-group requests received, checked against origin/main `0589e13f4`:
- hook-launcher:
- The README exec-form version and two-process shape, the PLAN.md
annotation, the relaxed launcher test pin, the PATH-first resolver cases
and the setup bash probe: applied (see Fix).
- Node declaration: already done here (README Requirements, setup check
item 3, `prerequisites.json`).
- Five owner decisions: #4390 already had its packet. #3683 was already
reopened and now has a ratify-or-reverse packet. #4118, #4681 and #4683
are now reopened with packets. #3686 belongs to hook-launcher.
- ci:
  - #4527 already carries the per-suite questions.
- The `\tmp\x` downgrade follow-up is not filed, because bca5f57 in
this PR restores the block assertion: the helper no longer downgrades a
backslash-led `\tmp`, and the quoted spellings expect 2.
- #3683: #5316's windows-2025 run shows `cloud-bootstrap-plugins`
PASS=79 with no probe skip, so its probe needs no rework. The results,
and #4845's causes marked as hypotheses, are on #3683.
- tracker:
  - #3951 not implemented: owner decision (packet above).
- #4235 not delivered by this PR; it needs its own dispatch. The owner's
comment closing #4817 asks for "a design scoped to #4235", which belongs
in its own PR: its `ps-command.sh` changes overlap this PR's classifier
edits and need the token-subset differential. The tracker posts the
scope comments.
  - #4678: reopened with Windows operator steps (not run here).
- #4679's render confirmation: already an open item with the exact
steps.
- The #4715 bypass: probed, not reproducible (see Verification). Its
payloads go to the scripts group rather than this PR, because the corpus
lives in `scripts/`.
- conventions:
- The setup apply bullet already carries main's scope-matching caveat 2.
- #4679: the condition-3 packet is posted. The wording goes to
conventions after the owner answers.
- source-control: commented on #4247 with #5317's widened matcher.
- performance: PLAN.md is already relabelled and #4390 already carries
both labels. The Done-when and `async` questions are posted as an
addendum.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01EugXnFddtpHcY5gTuyEirB

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(guardrails): refuse PowerShell here-string and bare-CR shapes hiding git

2 participants