fix(guardrails): cut block-hook-bypass messages to what the agent can act on (#4679) - #4711
Merged
Merged
Conversation
…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>
Contributor
|
PR body contract — issue linkage This PR body does not yet satisfy the issue-linkage contract:
Edit the body and this comment updates itself on the next run. |
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>
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>
1 task done
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> <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> </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> <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> </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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4679
Stacked on #4699 (
cursor/4528-guard-overlength-fastpath-37e9). Until that merges, the diff againstmainincludes its commits; review the commits afterd6963e5c1.Summary
block-hook-bypassrefusals drop operator lever spam and wrong scratch-root advice on PowerShell/python. stderr carries only actionable lines; levers go to onesystemMessageper session/agent (latched viaHOOK_NOTICE_KIND == full). README keeps the write-forms list. One-line README pointer remains until a human confirms exit-2systemMessagerenders.guardrails→ 0.38.0 (stacked above 0.37.3 from #4699).Test plan
/tmp(per implementer)systemMessageshows on exit-2 block