docs(guardrails): name 0.27.0, not 0.26.0, as the release that made block-hook-bypass operand-keyed - #2325
Conversation
|
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 finished @kyle-sexton's task in 40s —— View job Security Review
Scope:
FindingsNone. Every hunk in this diff is a version-number swap inside a comment, changelog prose paragraph, or Markdown caveat text. No executable line changed in
This PR is a pure documentation/comment correction (fixing a stale |
c8c8586 to
1f3b502
Compare
|
Claude finished @kyle-sexton's task in 57s —— View job Security Review
Scope:
FindingsNone. Every hunk in this diff is a version-number correction inside a comment, CHANGELOG prose paragraph, or Markdown caveat text — a documentation-accuracy follow-up to #2287, correcting stale
Pure documentation/comment correction with zero behavioral or trust-boundary impact. No security concerns to report. |
…lock-hook-bypass operand-keyed Follow-up to #2287 (merged). That work was written against 0.26.0 and renumbered to 0.27.0 when main took 0.26.0 for the block-dangerous-git / block-no-verify jq fail-closed change (#2146) mid-flight. The renumber reached the CHANGELOG heading, the manifest version and the entry's own comparison table; it did not reach the narrative around them, so eight prose references still send a reader to a release that documents something else: README.md the "Since 0.26.0" / "Before 0.26.0" pair block-hook-bypass.sh the note above scratch_target_exempt's fail-close the test file three section comments CHANGELOG.md the erratum inside 0.25.1, both mentions 0.26.0's own heading is untouched — it is the one correct 0.26.0 reference in this plugin, and re-grepping against current main confirms it is the only one left afterwards. Rebased onto current main, which moved a long way and took 0.27.1 for the stale-path-verify comment fix (#1555) while this branch sat. This release is therefore 0.27.2, not 0.27.1 as the pre-rebase branch had it. Comments and prose only. Verified mechanically rather than asserted: the diff of the two shell files contains ZERO non-comment changed lines. Refs #2226 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1f3b502 to
0f39cf2
Compare
|
Warning Automated security review did not complete — this is an infrastructure failure, not a review verdict. Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."
The check is green on purpose, and it is not evidence. It certifies that a security pass ran, and this one did not complete — but the cause is outside this PR's control, so merging is deliberately left unblocked rather than locking every merge for the length of the outage. Nothing was reviewed at this head. Where this check is required, it is satisfied without that evidence; a human should review security-sensitive changes here before merging. Re-run the job to retry the review; a new push also retries it only if the caller's Re-running does NOT help for every class:
|
|
Warning Automated review did not complete — this is an infrastructure failure, not a review verdict. Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."
Re-run the job to retry the review. A new push re-triggers this lane only if the caller's |
…e a reopened residual (#2367) ## Summary Three inline-write forms reached a real file through `block-hook-bypass.sh` without matching any detector (#2217). All three sit inside the guard's own declared threat model. One of them — the `python3 - <<PY` stdin heredoc — is a **documented accepted residual**, and this PR treats it as **reopening an accepted residual on new reachability evidence**, not as an oversight: the acceptance comment is updated in both files that carry it rather than contradicted. **Anchors are re-read at `4c90b454`.** The issue was filed before PR #2287 landed and cites `block-hook-bypass.sh:635-636` / `:670`; at the base of this branch those lines are `:968-969` and `:1005`. #2287's mark-based `strip_literals` rewrite is untouched by cause 1 and cause 3; cause 2 changes one join in the same function, in the direction #2287 deliberately left alone. PR #2325 is already merged into this base, so none of its renumbered lines are touched here. ### Cause 1 — the interpreter detector was a spelling floor, not a rule Both lanes required the **literal** `python3`: the Bash lane's `EXEC_LC` scan and the PowerShell lane's `ps::might_write_via_python3` token test. So the identical inline write spelled `python -c`, `py -c`, `py3 -c`, `python2 -c` or `python3.11 -c` ran unseen. The guard's own scope message advertised `python -c` as its example — naming the one spelling the regex did not match. The command word is now the python **family** (`py`/`python`/`pypy` + optional version suffix + optional `.exe`), still separator-anchored. `py -3 -c` is admitted because a `-<digits>` token cannot be a script path; no other gap between interpreter and flag is admitted, so `python3 build.py` and `python3 -m tool …` still do not block. ### Cause 2 — a physical newline inside a quoted span split a producer from its own redirect A newline reached with a quote span still **open** is not a separator: bash is inside a quoted word, so the text either side of the span is **one word**. `strip_literals` re-emitted it anyway, `normalize_segments` split there, and `producer_redirect_bypass` requires producer and redirect in one segment. The join is now **empty**. Not a space — and that is the one place this PR's mechanism differs from the one the issue suggested. `ec"<newline>"ho x > f` is `echo x > f` to bash; a space join leaves `ec ho`, which `_producer_head` does not match, so the write is still missed. That case ships as an assertion so the choice is evidenced rather than argued. Joining empty cannot manufacture a token bash does not also form, because an open quote is precisely what makes the two sides one word. ### Cause 3 — REOPENED ACCEPTED RESIDUAL (`RECONCILE.md` AD-12) `python3 - <<PY … PY` (no `-c`) was recorded as uncovered and accepted in the PowerShell lane's comment. Reachability evidence established before changing it, by grep over the repo: - `.work/handoffs/20260809T082720Z-handoff-post-2008-followups.md:211` — a prior session in this repository reached for exactly that form (`python - <<'PY'`) **to patch a file**. Note the spelling is `python`, not `python3`, so it is a cause-1 datum too. - The reporting session hit it while trying to comply with this guard's own remediation. - Cause 1 raises the pressure toward it: a refused `python -c` write reroutes most naturally to the heredoc. The `-` is what makes it inline — the code sits in the command string the hook reads, not in an opaque script file. `strip_literals` drops the heredoc operator and body, so `EXEC_LC` retains `python3 -` while the body's write indicators stay visible in `COMMAND_LC`. **Narrowed residual, restated at its real width:** `python3 <<PY` with **no** `-` stays uncovered. Matching a bare trailing interpreter token would flip `echo "pathlib" | python3` and `cat script.py | python3` to blocked. Both floors are asserted. ### Direction of every behavior change **23 granted → refused, 1 refused → granted.** Measured two ways, not asserted. **Tier 1 — the shipped suite, run against the PRE-change hook.** The new assertions were copied into a worktree at the merge base and the suite executed there: ``` $ bash plugins/guardrails/hooks/block-hook-bypass.test.sh # pre-change hook, new assertions FAIL: #2217: python -c open write (blocked): expected exit 2, got 0 FAIL: #2217: py -c open write (blocked): expected exit 2, got 0 FAIL: #2217: py3 -c open write (blocked): expected exit 2, got 0 FAIL: #2217: python2 -c open write (blocked): expected exit 2, got 0 FAIL: #2217: python3.11 -c open write (blocked): expected exit 2, got 0 FAIL: #2217: pypy3 -c open write (blocked): expected exit 2, got 0 FAIL: #2217: py -3 -c open write (blocked): expected exit 2, got 0 FAIL: #2217: path-qualified python.exe -c open write (blocked): expected exit 2, got 0 FAIL: #2217: printf, physical newline in a single-quoted arg (blocked): expected exit 2, got 0 FAIL: #2217: echo, physical newline in a double-quoted arg (blocked): expected exit 2, got 0 FAIL: #2217: quote span splicing a command word (blocked): expected exit 2, got 0 FAIL: #2217: python3 - <<PY heredoc write (blocked): expected exit 2, got 0 FAIL: #2217: python - <<PY heredoc write, family spelling (blocked): expected exit 2, got 0 FAIL: #2217: PS python -c open write (blocked): expected exit 2, got 0 FAIL: #2217: PS py -c open write (blocked): expected exit 2, got 0 PASS=377 FAIL=15 ``` Every failure is `expected 2, got 0` — granted → refused. None is `expected 0, got 2`. **Tier 2 — adversarial probes written after the suite was green, specifically hunting the other direction.** Eight more rows move; all eight are now assertions too: | row | before | after | direction | |---|---|---|---| | `/usr/bin/python -c "open('f','w')…"` | 0 | 2 | granted → refused | | `echo "a<NL>" x > f` | 0 | 2 | granted → refused | | `cat "a<NL>" > f` | 0 | 2 | granted → refused | | `echo "a<NL>b" "c<NL>d" > f` | 0 | 2 | granted → refused | | `if true ; then echo "a<NL>b" > f ; fi` | 0 | 2 | granted → refused | | `! echo "a<NL>b" > f` | 0 | 2 | granted → refused | | `exec -a n echo "a<NL>b" > f` | 0 | 2 | granted → refused | | `FOO=1 printf "a<NL>b" > f` | 0 | 2 | granted → refused | | **`foo "a<NL>" echo x > f`** | **2** | **0** | **refused → granted** | **The one refused → granted, named rather than buried.** Fusing the two sides of a span back into one segment also puts whatever preceded the span at the segment start, where `_producer_head`'s `^` anchor sees it. In `foo "a<newline>" echo x > f`, bash's command word is `foo` and `echo` is one of its *arguments*, so the redirect's producer is another program — and this guard is producer-scoped by design (see the README's producer-scoping note). The newline previously split it into a bogus `echo x > f` segment and blocked it. The single-line spelling `foo "a" echo x > f` is **rc=0 on `main` today**, so this makes the multi-line form agree with shipped behavior rather than inventing an exemption. Both are asserted. **It is one row, not a class — verified, not reasoned.** The obvious escalation is a *legitimate* command prefix in front of the span hiding a real producer from the `^` anchor. Every prefix the file already models was probed against both hooks and all of them still block, because `_cmd_prefix` / `_modifier_opt_arg` / `_leading_redir` peel on the fused segment: env assignments, `env`, `if…then`, `!`, `exec -a NAME`, and a leading redirect (rows 5–8 above, each paired with its single-line control). The only text that survives to the segment start is a genuine command word, which is exactly the case where the producer is not `echo`. Every remaining floor keeps `rc=0`: the name anchor, the #1601/#2148 over-block repros re-run for each new spelling, the multi-line prose/`--body` floor, the `/dev/null` discard floor and the stdin floor. ## Test plan Hook invoked as a decision function on `PreToolUse` Bash payloads built with `jq -n --arg` — `rc=2` blocked, `rc=0` allowed. Adversarial-first: every row below was written and run against the **pre-change** hook first. **Before (`4c90b454`) → after (this branch):** ``` BEFORE AFTER ### CONTROL (guard live) echo "*" > .gitignore rc=2 rc=2 git status rc=0 rc=0 ### C-H1 interpreter spelling floor (granted -> refused) python3 -c open-write rc=2 rc=2 python -c open-write rc=0 rc=2 py -c open-write rc=0 rc=2 python3.11 -c open-write rc=0 rc=2 python2 -c open-write rc=0 rc=2 py3 -c open-write rc=0 rc=2 py -3 -c open-write rc=0 rc=2 /usr/bin/python -c open-write rc=0 rc=2 /c/Python313/python.exe -c open-write rc=0 rc=2 pypy3 -c open-write rc=0 rc=2 ### C-H1 name-anchor floor (unchanged) notpython3 -c write rc=0 rc=0 mypython3 -c write rc=0 rc=0 pythonx -c write rc=0 rc=0 mypy -c write rc=0 rc=0 happy -c write rc=0 rc=0 spy -c write rc=0 rc=0 pytest -c write rc=0 rc=0 ### over-block floor #1601 / #2148 (unchanged) #1601 read-only json.load(open(p)) rc=0 rc=0 #2148 print-only rc=0 rc=0 python -c read-only open rc=0 rc=0 py -c print only rc=0 rc=0 python3.11 -c os.path.normpath rc=0 rc=0 python -m tool rc=0 rc=0 python build.py rc=0 rc=0 python --version rc=0 rc=0 py --list rc=0 rc=0 ### C-H3 newline-split producer (granted -> refused) printf 'a<NL>b<NL>' > notes.md rc=0 rc=2 echo "a<NL>b" > notes.md rc=0 rc=2 ec"<NL>"ho x > f (one bash word = echo) rc=0 rc=2 printf "a\nb\n" > notes.md (escaped control) rc=2 rc=2 cat > f with an earlier multi-line quote rc=2 rc=2 ### C-H3 blast-radius floor (unchanged) gh pr --body multiline mentioning echo > f rc=0 rc=0 git commit -m multiline prose mentioning cat > f rc=0 rc=0 grep "foo<NL>bar" file | wc -l rc=0 rc=0 multi-line span discarded to /dev/null rc=0 rc=0 multi-line span then a real producer+redirect rc=2 rc=2 multi-line span piped to wc rc=0 rc=0 multi-line sq span in --body, no redirect rc=0 rc=0 unterminated quote at end of command rc=0 rc=0 ### G1 reopened residual (granted -> refused) python3 - <<PY open-write rc=0 rc=2 python - <<PY open-write rc=0 rc=2 ### G1 stdin floor (unchanged) python3 <<PY open-write (no dash) [residual] rc=0 rc=0 python3 - <<PY read-only rc=0 rc=0 cat s.py | python3 - rc=0 rc=0 cat s.py | python3 rc=0 rc=0 echo "pathlib" | python3 rc=0 rc=0 python3 - </dev/null rc=0 rc=0 commit message quoting a heredoc write rc=0 rc=0 cat <<EOF > file (cat lane control) rc=2 rc=2 ``` **Shipped contract suite** — `bash plugins/guardrails/hooks/block-hook-bypass.test.sh`. Counts are pasted from the runs in the "Suite counts" comment below; no existing assertion changed. **Lint / repo checks run locally:** ``` $ shellcheck -x plugins/guardrails/hooks/block-hook-bypass.sh \ plugins/guardrails/hooks/block-hook-bypass.test.sh \ plugins/guardrails/lib/powershell/ps-command.sh (no output) $ shfmt -d <same three files> (no output) $ scripts/check-changelog-parity.sh --check Every versioned plugin has a CHANGELOG.md (or a stale-guarded baseline entry), and none documents a version above its manifest. $ scripts/check-changelog-parity.sh --check-order All 76 changelog(s) read newest-first with no duplicate versions. $ scripts/check-changelog-parity.sh --check-bump origin/main Every plugin whose version changed vs origin/main has a '## [<version>]' CHANGELOG.md entry. ``` ### Not a sign-off Per `OUTCOME.md`, the required `security-review` check has been observed reporting **pass in 16s on a ~700-line change to this same hook** (run `31558511903`) while the reviewer inside it did not run. A green `security-review` on this PR should not be read as a security review of it. This is a change to a guard whose whole job is refusing bypasses and it wants human eyes on the diff. ## Related Closes #2217. Inbox items: `2026-08-10-plugin-quality-audit-four-components` (C-H1, C-H3) and the `20260811-021645`-routed `audit-pass` report-path item (G1). Ledgers: `.work/handoff-inbox-batch-4/ledgers/I7-four-components-023241Z.md` §§ C-H1, C-H3; `.work/handoff-inbox-batch-4/ledgers/I8-audit-pass-report-path.md` § G1. Adjudication: `RECONCILE.md` AD-12. Adjacent and deliberately **not** closed by this PR: **#1601** and **#2148** report this same arm *over*-blocking. Both were re-verified `rc=0` at the base of this branch (the write-mode discrimination already fixed the mechanism #1601 names), and both repros are pinned as floors here — for the new spellings as well — so this widening does not reopen them. They stay open on their own terms. **#2227 needs no work: already shipped.** `repo_oid_width` at `origin/main` captures the git error via `2>&1`, caches only a successful width (`_repo_oid_width_key=""` on failure), and blocks with a distinct message via `_lease_oid_width_unknown` — PR #2288, commit `f3336eab`, `plugins/guardrails/CHANGELOG.md:134-135`. The issue is CLOSED/COMPLETED. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Follow-up to #2287 (merged), which fixed #2226 by making
block-hook-bypass's exemptionsoperand-keyed. That work was written against 0.26.0 and renumbered to 0.27.0 mid-flight,
because
maintook 0.26.0 for theblock-dangerous-git/block-no-verifyjqfail-closed change(#2146) while the branch was open.
The renumber reached the CHANGELOG heading, the manifest version and the entry's own comparison
table. It did not reach the narrative around them, so eight prose references shipped on
mainstill send a reader to a release that documents something else entirely:
plugins/guardrails/README.mdplugins/guardrails/hooks/block-hook-bypass.shscratch_target_exempt's fail-closeplugins/guardrails/hooks/block-hook-bypass.test.shplugins/guardrails/CHANGELOG.mdmain's own 0.26.0 entry — thejqfail-closed release — is untouched. It is the one 0.26.0reference in this plugin that is correct, and after this change it is the only one left.
Comments and prose only. No behaviour change, no assertion moved, no gate output changed.
Rebased, and renumbered again
This branch was
CONFLICTINGagainstmainand has been rebased onto currentorigin/main(
900d33a1at the time of the rebase).maintook 0.27.1 for thestale-path-verify.test.shcomment fix (#1555) while this branch sat, so this release is 0.27.2, not 0.27.1 as the
pre-rebase branch had it. Patch, docs only — the precedent 0.25.1 set for a docs-accuracy correction
against a shipped entry.
The sweep's target set was re-grepped against current
mainrather than trusted from the branch,since a further version move could have changed which references are stale. It did not: the same
eight, in the same four files. Enumerated on
origin/mainbefore the rebase:Test plan
The claim is that nothing executable moved, so that is what is verified — mechanically, not asserted.
Every changed line in both shell files is a comment line:
Only
0.26.0remains where it should:Gates, run locally in the worktree after the rebase:
The
block-hook-bypasscontract suite was green at 345 / 345 on the tree #2287 merged, andplugin-gateconfirmed that on ubuntu-24.04 / bash 5.2.21. This branch changes no executable line ofeither file, so
ciruns it again unchanged here.Related
Follows up #2287 (merged) and its issue #2226 (closed) — the quoted-redirect-operand fix whose
release number this corrects.
Version-collision context: #2146 landed as
guardrails0.26.0 while #2287 was open, and #1555 landedas 0.27.1 while this branch was open.
Origin: handoff-inbox batch 4, lane owning
guardrails. Filed as a follow-up because #2287 wasmerged before this sweep could be pushed to its branch.