Skip to content

fix(guardrails): cut block-hook-bypass messages to what the agent can act on (#4679) - #4711

Merged
cursor[bot] merged 13 commits into
mainfrom
cursor/4679-bypass-message-cut-37e9
Sep 27, 2026
Merged

cursor[bot] merged 13 commits into
mainfrom
cursor/4679-bypass-message-cut-37e9

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Closes #4679

Stacked on #4699 (cursor/4528-guard-overlength-fastpath-37e9). Until that merges, the diff against main includes its commits; review the commits after d6963e5c1.

Summary

block-hook-bypass refusals drop operator lever spam and wrong scratch-root advice on PowerShell/python. stderr carries only actionable lines; levers go to one systemMessage per session/agent (latched via HOOK_NOTICE_KIND == full). README keeps the write-forms list. One-line README pointer remains until a human confirms exit-2 systemMessage renders.

guardrails → 0.38.0 (stacked above 0.37.3 from #4699).

Test plan

  • block-hook-bypass 699/0 outside /tmp (per implementer)
  • Interactive: confirm systemMessage shows on exit-2 block
Open in Web Open in Cursor 

cursoragent and others added 8 commits September 27, 2026 21:32
…and parse once, linearly (#4528)

The Bash|PowerShell PreToolUse row ran past its 60-second hooks.json
timeout on a long command, and Claude Code lets a timed-out PreToolUse
command hook's tool call proceed, so the command passed every guard
unchecked. On main: 7.23 s at 10 KB, 12.6 s at 16 KB, still running at
120 s for ~70 KB. Now 132 ms, 203 ms and 64 ms.

- lib/hook-utils.sh: hook::bash_parse_segments split the command with
  one ${cmd:i:1} per character, which bash answers by measuring the whole
  string, so every parse was quadratic. It now splits in 4096- and 64-byte
  blocks under the C locale; output is byte-identical. The body moves to
  hook::bash_parse_segments_uncached behind a thin wrapper (the
  hook::jq_fields split). Synced to the 17 carriers, each patch-bumped.
- run-guards.sh: --max-command-len <n> ends the chain at the first guard
  that blocks a command longer than <n>. The Bash row passes 16384, the
  MAX_COMMAND_LEN five of its guards already refuse unread; block-no-verify
  runs first. Kill switches are unchanged: a disabled guard exits before
  its ceiling and the next one blocks.
- run-guards.sh: the event's command is tokenized once and the record is
  replayed to every later guard that parses it; a parse cut short by a
  guard's exit is not kept.

Tests: run-guards.test.sh covers the short-circuit (stub and shipped row,
kill switch, unprimed payload, boundary, bad value), the row/guard ceiling
parity, and the shared parse (equality with a standalone parse, one walk,
cut-short parse). lib/hook-utils.test.sh covers the byte walk, locale
restore, and an 8x-size cost ratio (7x now, 45x on main).

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
…rlength-fastpath-37e9

# Conflicts:
#	plugins/source-control/CHANGELOG.md

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
validate-plugin-contracts forbids the autonomy plugin from naming the
org or fleet repos.

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
… command (#4528)

The 0.37.3 entry said four guards with no ceiling each tokenized the
whole command; flag-commit-pr-skill-bypass never calls the tokenizer and
is off by default. A per-guard profile of main on a 70 KB command shows
block-hook-bypass, block-noncanonical-commit and
block-convention-violation at about 44 s each.

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
… act on (#4679)

stderr now carries the verdict, the Write/Edit remedy, and on the redirect
and staged-move lanes the reason the target was not scratch-exempt plus the
roots that would have exempted it. The PowerShell and python lanes name no
scratch root. The operator levers move to one systemMessage per session and
agent, latched by hook::notice_once with its renewal declined; the scope
note moves to the README. Corrects the stale 'systemMessage is discarded on
exit 2' claims.

Refs #4118

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
… suite's others are

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
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.

cursoragent and others added 2 commits September 27, 2026 23:07
The DLSS selftest's user-only Set-Acl deny still lets an elevated runner
list the folder, and Windows often leaves the error TargetObject empty, so
the four unreadable-folder assertions never match.

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

The branch carries the linear-parse tests. Shellcheck flags the LC_ALL
subshells that the tests exist to check, and the portability gate flags a
grep -F fixture token.

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@cursor
cursor Bot marked this pull request as ready for review September 27, 2026 23:15
cursoragent and others added 3 commits September 27, 2026 23:18
DirectoryInfo.SetAccessControl is absent in PowerShell 7, so the Windows
selftest threw before the deny cases ran. Write the same user,
Administrators, and Everyone list deny through FileSystemAclExtensions.

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
A Deny ACE, including one for Everyone, still lets the elevated Windows
runner list the folder. Hold the directory open with no sharing so the
next Get-ChildItem fails with a sharing violation, and recover a quoted
path from the error message when TargetObject is empty.

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
The runner's pwsh has no FileOptions.BackupSemantics, so the selftest
threw before the sharing lock was taken. Open the directory with
CreateFileW and FILE_FLAG_BACKUP_SEMANTICS instead.

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@cursor
cursor Bot merged commit 9d58f60 into main Sep 27, 2026
19 checks passed
@cursor
cursor Bot deleted the cursor/4679-bypass-message-cut-37e9 branch September 27, 2026 23:46
cursor Bot pushed a commit that referenced this pull request Sep 28, 2026
…ed (#4678) (#4716)

<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
Closes #4678

## Summary

On Windows, bare redirects (and D1 secret-pattern Writes) to 8.3 temp
spellings like `C:/Users/ABCDEF~1/...` are exempt when physically under
a real temp root and the project is outside temp. Mixed-spelling staged
moves block both ways. Non-8.3 `~` forms still refuse. POSIX unchanged.

`guardrails` → **0.37.7**. May conflict with #4711 (`block-hook-bypass`)
/ #4709 version claim — serialize on merge.

## Test plan

- [x] Sim + replay 1006 cmds; Windows-only cases SKIP without Windows
host (per implementer)
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-ab53da24-b89d-4314-a060-0da474e837e9?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-ab53da24-b89d-4314-a060-0da474e837e9&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request Sep 28, 2026
…eral (#4118) (#4766)

<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
Closes #4118

## Summary

On the `cat`, `echo`/`printf` and staged-move lanes, the block message's
roots line said "A bare target under these roots is exempt: ..., the OS
temp directory." It never said the target must be spelled literally. So
an agent blocked on a project path was offered the temp tree, tried
`$TMPDIR/x` or a quoted path, and was blocked a second time before it
learned the rule. This builds on the message cut from #4679 / #4711
(merged), which already lists the temp tree only when the project root
is outside it and gives one reason line per refusal code. It changes one
line and adds no second line.

## Fix

- `block_bypass()`: the roots line now reads `An unquoted literal
<target|move destination> under these roots is exempt: <roots>; a quoted
or variable-carried one never is.`
- The `; a quoted or variable-carried one never is` tail is dropped when
the reason line above already says it (refusal codes `quoted` and
`unnormalized`), so #4711's rule against repeating a line still holds.
- No parser change and no hot-path work: the branch runs only after a
block has been decided. `.claude/rules/hook-budget.md`'s constraint is
respected.
- `README.md` block-message entry, `CHANGELOG.md` 0.38.10, and
`plugin.json` 0.38.9 to 0.38.10.

## Verification

- `bash plugins/guardrails/hooks/block-hook-bypass.test.sh` run from a
copy outside `/tmp`: `PASS=739 FAIL=0` (on `main`: 737/0). The two new
assertions pin the full line for a literal target outside every root and
the repeat-free line for a `/tmp/$f` target. The updated assertions
cover the quoted-scratchpad target and the staged move destination.
- Run inside `/tmp/wt-4118`, one case fails (`symlink: a genuine temp
write in the same root stays allowed`). That case depends on where the
suite runs: with the checkout under the temp tree, the temp default
correctly turns off. The same case passes from a path outside `/tmp`,
which is how #4711 ran it.
- `scripts/check-changelog-parity.sh --check-bump origin/main`: ok.
- `shellcheck` clean on the hook and its test; `markdownlint-cli2` clean
on README and CHANGELOG. `scripts/ai-slop-report.sh origin/main` found
no findings in 2 files; `scripts/check-purged-em-dashes.sh`: ok.

## Related

- #4679 / #4711: the message cut this aligns with. #4679's body records
that its reason and roots lines were meant to cover #4118. This PR
closes the part they left open: the roots line did not say the target
must be literal.
- #4113 (parent), #3719 (shipped temp-tree default).

### Decision record

- **Claim:** A literal path under the host temp tree is exempt with no
configuration only when the project root is outside the temp tree. A
`$`-carrying, backtick, `~`, or quoted operand is never exempt (8.3
short-name components excepted, #4678).
- **Basis:** `_bbh_temp_default_applies`, the `quoted` / `unnormalized`
refusal codes and `_bbh_exempt_roots` in
`plugins/guardrails/hooks/block-hook-bypass.sh`, and the pinned
`reason_is` cases in `block-hook-bypass.test.sh`.
- **As of:** `origin/main` `0fc60e845`, 2026-09-28.
- **Recheck:** when `scratch_target_exempt` or `_bbh_refusal_line` gains
or drops a code, or the temp default's gate changes.
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-ab53da24-b89d-4314-a060-0da474e837e9?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-ab53da24-b89d-4314-a060-0da474e837e9&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
kyle-sexton added a commit that referenced this pull request Sep 29, 2026
…5306)

Closes #4676

## Summary

- #4676 AC3 asked for the statusline tee's measured per-render process
count, or a statement that no diet was needed. #4719 (0.8.30) shipped
AC1 and AC2 and closed the issue without either. This PR records the
counts: the render path spawns 0 processes, the elected drain spawns 5
to 12 once per drain interval, so no diet was needed.
- Five released CHANGELOG entries (0.8.29, 0.8.31, 0.8.33, 0.8.34,
0.8.35) described shared code this plugin never calls. They now say so
in one line. Node.js is now declared as a requirement.

## Fix

- `bench/trace-probe.sh --count` reports processes spawned per render
shape (external commands and subshell forks, counted by pid with
pid-logging stubs on `PATH`). `bench/bench.test.sh` gains four `--count`
cases, including one that fails on an unstubbed external.
`bench/README.md` records the table and the method.
- No tee code changed. Version 0.8.36 to 0.8.37.
- In-place corrections of released CHANGELOG entries, each reduced to a
one-line sync note:
- 0.8.29 (quadratic `hook::bash_parse_segments` prose; also dropped the
#4528 link, since 0.8.29 shipped in PR #4711 for #4679)
  - 0.8.31 (`wsl` operand parsing prose)
  - 0.8.34 (`hook::repo_relative_path_to` prose)
- 0.8.35 (prerequisite notice and format-hook prose; this plugin has no
format hooks or `prerequisites.json`)

- 0.8.33 (shared launcher prose that claimed the hook rows were
unchanged after 0.8.32 rewrote them), reduced to "Shared
launcher/library sync; no change to this plugin's behavior."

The 0.8.37 entry names the same five corrections, which is what
`check-changelog-parity.sh` requires for an in-place edit.
- Declared Node.js: README Requirements now lists it and
`skills/setup/SKILL.md` `check` step 1 probes `node` (a pre-computed
`command -v node` row, so it works without the launcher), because the
`StopFailure` row runs through `node hooks/exec-bash.mjs`. The launcher
itself is unchanged.
## Verification

Run in the worktree after merging origin/main:

- `bash plugins/rate-limit-guard/bench/trace-probe.sh --count`:
non-elected unchanged input 0, non-elected changed input 0, elected
drain (no-change skip) 5, snapshot rewritten 8, orphan sweep due 9,
first render on a machine 12.
- `BENCH_LANES=1 bash plugins/rate-limit-guard/bench/bench.test.sh`:
PASS=17 FAIL=0.
- `bash plugins/rate-limit-guard/scripts/statusline-tee.test.sh`:
PASS=146 FAIL=0.
- `bash plugins/rate-limit-guard/scripts/statusline-shim.test.sh`:
passed 104, failed 0.
- `bash plugins/rate-limit-guard/hooks/record-rate-limit-stop.test.sh`:
PASS=19 FAIL=0.
- `scripts/check-changelog-parity.sh --check --check-order`: clean.
- `scripts/validate-plugins.sh`: all manifests and the catalog
validated.
- `scripts/check-changed-skills.sh origin/main`: setup skill PASS, 0
errors.
- Confirmed `record-rate-limit-stop.sh` calls only
`hook::buffer_stdin_to`, `hook::json_escape` and `hook::append_jsonl`
from hook-utils.sh.

Wall clock on this host (WSL2, `bench-idle.sh 21`, throwaway HOME):
median 5 to 6 ms per render. Not comparable to the Windows figures;
process counts are the portable claim. Not measured: the bash older than
4.2 fallback (macOS `/bin/bash` 3.2), which this host cannot run.

## Related

- Audit finding for #4676 (REPORT.md row 3b) and the
`plugin-rate-limit-guard` changelog finding: `.work/audit/REPORT.md`.
- Sibling: #4721 (context-guard tee), a separate plugin; #4675 is its
closed context-guard issue.
- #4719 shipped AC1 and AC2 of #4676.
- Cross-group requests applied: hook-launcher F48 (0.8.33 wording and
Node.js declaration; the launcher code ships in #5309). Tracker F88 was
already met by this PR (0.8.37 CHANGELOG and `bench/README.md` state the
measured counts).

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

https://claude.ai/code/session_01EugXnFddtpHcY5gTuyEirB

---------

Co-authored-by: Claude Opus 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): cut block-hook-bypass messages to what the agent can act on

2 participants