Repository navigation
feat(guardrails): block a PR merge while review threads, change requests or failing checks remain - #7056
Conversation
…sts or failing checks remain Add the block-unresolved-pr-merge guard (#6463). It matches every common merge path: gh pr merge (--admin and --auto included), a PUT to the REST merge route or the Claude Code cloud ccr/auto_merge route through gh api or curl, the mergePullRequest, enablePullRequestAutoMerge and enqueuePullRequest GraphQL mutations, and an MCP merge_pull_request or enable_pr_auto_merge tool under any server prefix. PR state is read over REST so the guard works where GraphQL is refused; review-thread resolution comes from GraphQL where served, else the cloud ccr/review_threads route. Pending checks block an immediate merge only, since auto-merge and the merge queue wait for checks by design. The guard fails open with a notice on both channels when gh is missing or a read fails. Off switch: block_unresolved_pr_merge_enabled. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L8KdG7fWg2YrErVYWtohmR
|
The lanes run once the PR is marked ready. That flip waits for Kyle, because it also starts the review lanes. Until then, the local runs listed under Verification in the PR body are the signal. Generated by Claude Code |
… and prefix gaps From review of #7056: - A GraphQL merge, auto-merge or merge-queue mutation whose PR cannot be resolved (id from $ID, "$PR_ID", $(jq ...), an unreadable node, no gh) now fails closed, with the literal-id fix in the message. Tests cover the source-control 4.2.2 enqueue shape from #7075. - gh pr merge --help and -h are not merges; stay-quiet tests assert no gh call. - All reads share one 35 s budget, each call capped at what is left and at 15 s, enforced by timeout or, where it is missing, a watcher process, so the shared row stays under its 60 s timeout. - Leading reserved words (then, do, {, ...) and the command, time, exec and nohup prefixes are stepped over, and gh pr -R o/r merge is read. - The fail-open notice says the merge will run unchecked. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L8KdG7fWg2YrErVYWtohmR
GUARDRAILS_BUPM_BUDGET_S let the environment shorten the guard's 35 s read budget. settings.json `env` reaches a hook's environment, so a project setting could make every merge run unchecked. The budget is now a constant; the budget tests run a copy of the hook with that one line rewritten, and a new case proves an env var cannot shorten it (fails before this change). Refs: #6463 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L8KdG7fWg2YrErVYWtohmR
The guarded literal merge (a gh pr view --jq gate piped to grep -qx true, then gh pr merge <N> -R <repo> --match-head-commit <sha>) passes on a clean PR and is blocked while a review thread is open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVW9hAwnSBrnFsR3kzXcB4
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Claude finished @kyle-sexton's task in 3m 36s —— View job Security review in progress
|
Remove "stated plainly" from a bold lead-in and "honest" from "the honest follow-up". Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Claude finished @melodic-automation-lanes[bot]'s task in 6m 2s —— View job Code review complete
SummaryScope: correctness/maintainability only — a separate This is a large, carefully engineered guard with an unusually thorough test suite (454 lines covering every merge path, both shells, MCP, the budget, and the fail-open/fail-closed split). One finding, posted inline at The GraphQL node-id-to-PR# resolution used by the fail-closed path (a The PR author already flagged two comparable open design questions for the human reviewer (items 5 and 6 in the body, re: fail-closed scope and the #7075 interaction) — this is a related but distinct gap in the same fail-closed path, in the cloud environment the PR itself targets. claude-lane-summary: lane=claude-review sha=7e0a830bf7059ed6241fdfe01ae642bbf34fc4c3 run=38403508609 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8a1a5f1d10
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Claude finished @melodic-automation-lanes[bot]'s task in 9m 47s —— View job Security review complete
2 findings, both posted inline and independently confirmed:
No other issues met the high-signal bar. GitHub Actions hardening is out of scope for this lane (deferred to zizmor); no workflow files changed in this PR anyway. claude-lane-summary: lane=claude-security-review sha=7e0a830bf7059ed6241fdfe01ae642bbf34fc4c3 run=38403508578 |
|
claude-review has reviewed this pull request through 99a4345; a later push is reviewed from there. |
|
claude-security-review has reviewed this pull request through 99a4345; a later push is reviewed from there. |
…T merges - A GraphQL merge mutation is checked against the node id bound to pullRequestId (a literal, or a variable whose literal value is a -f argument or a JSON variables entry), not the first PR_ token anywhere in the command. A decoy id in another field no longer redirects the check; no binding, or two different bound ids, fails closed. - curl -T / --upload-file to the merge route counts as a PUT unless -X names another method. - Quoted review-thread text carries the full untrusted-content spine. - The header and README say every GraphQL merge mutation is blocked in a Claude Code cloud session, where GraphQL is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVW9hAwnSBrnFsR3kzXcB4
|
Claude finished @kyle-sexton's task in 5m 19s —— View job Security review complete
SummaryScope: incremental review of what changed since
No new findings. I did not find an exploitable fail-open path introduced by this diff's node-resolution rewrite, the curl-PUT change, or the message-text change. Note: this sandbox's tool allowlist doesn't permit running claude-lane-summary: lane=claude-security-review sha=ab04e8513d717dec6e871531f33ae7b70ecd4252 run=38405911974 |
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Claude finished @melodic-automation-lanes[bot]'s task in 8m 18s —— View job Code review complete
SummaryScope: correctness/maintainability only (security-shaped observations are left to the parallel The One finding, posted inline at The new No other issues met the high-signal bar in the changed lines. claude-lane-summary: lane=claude-review sha=9e418b23f0a8bb5625d622f4482aade47ea94fc7 run=38406287873 |
…esolves The curl graphql cases now check the fake gh was asked to resolve the bound id itself, for plain and JSON-escaped "variables" bodies, rather than that some GraphQL call happened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVW9hAwnSBrnFsR3kzXcB4
|
Claude finished @kyle-sexton's task in 7m 22s —— View job Code review complete
SummaryScope: incremental review of what changed since One finding, posted inline at The new/tightened assertions ( I don't have a sandbox that permits running the test suite (or any ad hoc Bash reproduction) in this claude-lane-summary: lane=claude-review sha=99a43458a1ab34fc090aeec5e16316cefa230f6d run=38407989620 |
|
Claude finished @kyle-sexton's task in 1m 31s —— View job Security review complete
SummaryScope: incremental review of what changed since this lane's last review ( The diff is four lines: it tightens one existing assertion (checking for No security findings in this file's changes. Note for the record, not a finding of this lane: the parallel claude-lane-summary: lane=claude-security-review sha=99a43458a1ab34fc090aeec5e16316cefa230f6d run=38407989850 |

Requested by Kyle · project thread
Closes #6463
Summary
Before: nothing checked a direct
gh pr merge, REST or GraphQL merge, or MCP merge call. An agent could merge (or ask for--admin) without reading an unresolved review thread.After: a new guardrails guard,
block-unresolved-pr-merge, blocks a merge while the PR has an unresolved review thread, a reviewer whose latest decisive review requests changes, or a failed check run or commit status on its head. The block message lists each thread (path:line, first line of the comment, link), each changes-requested reviewer and each failing check, and names the fix.--adminand--autoare held to the same rule.Fix
plugins/guardrails/hooks/block-unresolved-pr-merge.sh, on the existing Bash|PowerShellrun-guardsrow plus a new MCP row (^mcp__.+__(merge_pull_request|enable_pr_auto_merge)$). Matches the scope update's table:gh pr merge(all flags),PUT repos/{o}/{r}/pulls/{n}/mergeviagh apiorcurl, themergePullRequest/enablePullRequestAutoMerge/enqueuePullRequestmutations viagh api graphqlorcurl, and the MCP merge tools. Matching runs on parsed argv at a command position, so a quoted mention (commit message, echo, heredoc) stays quiet. Commands that name neithermergenorenqueueleave after one string test.pulls/{n},pulls/{n}/reviews,commits/{sha}/check-runs,commits/{sha}/status. Thread resolution has no plain REST route: GraphQLreviewThreadswhere served, else the cloud routeGET pulls/{n}/ccr/review_threadsnamed in that 403 body (aCLAUDE_CODE_REMOTEsession tries the cloud route first). The same 403 body namesPUT pulls/{n}/ccr/auto_mergeas the cloud auto-merge path, so that route is matched too.ghis missing or unauthenticated, a read fails, the read budget runs out, or the PR cannot be identified.$ID,"$PR_ID",$(jq ...)(the source-control merge reference 4.2.2 enqueue shape from fix(source-control): confirm a merge-queue PR entered the queue after gh pr merge #7075), a node that can't be read, or no gh. The block message says how to fix it: pass a literal-f id=PR_...or usegh pr merge <n>.timeoutwhere it exists, and by a watcher process where it doesn't (stock macOS). No environment variable changes the budget:settings.jsonenvreaches a hook's environment, so a knob could make every merge run unchecked. The budget tests run a copy of the hook with its one budget line rewritten.then,do,{, ...), and thecommand/time [-p]/exec/nohupprefixes. It readsgh pr -R o/r merge.gh pr merge --help/-hand--disable-autoare not merges. The guard header lists the remaining gaps: wrappers likesudo/env/xargs, child shells,$(...), and--inputmutation files.guard-requires.sh(MCP inputs declared unprimed),hooks.json,plugin.jsonuserConfigblock_unresolved_pr_merge_enabled(default on), README guard/option tables, changelog fragment (minor).Defaults I picked where the issue left a detail open (please confirm):
gh pr readyis not matched. The 2026-10-06 scope update dropped the advisory and its table lists merge paths only; the issue title still says "and gh pr ready". Adding it is one line if wanted.--auto,enablePullRequestAutoMerge,enqueuePullRequest,enable_pr_auto_merge,ccr/auto_merge), since those wait for checks by design. Failed checks, threads and change requests block all paths.Open for Kyle (found by the overnight verifier, not changed here):
5. The fail-closed GraphQL rule also covers gh missing or unauthenticated. A mutation with a literal
PR_...id whose node can't be read is blocked. That covers no gh, a 401, and the cloud GraphQL 403. The issue's acceptance says the guard fails open when gh is missing or unauthenticated. Option A: keep it, since the mutation can't be checked. Option B: fail closed only for a non-literal id and fail open, with the notice, for a literal one.6. Interaction with #7075. Its source-control merge reference 4.2.2 passes the enqueue id through
$(jq ...). With this guard on, that step is always blocked, even on a clean PR (verified against branchfix/6630-merge-confirm-queue-entry). One side must change before both merge: #7075 passes a literal id or usesgh pr merge, or this guard relaxes the rule.Verification
bash plugins/guardrails/hooks/block-unresolved-pr-merge.test.sh-> 162 passed, 0 failed (at 96f4c12). Since the first push, the suite also covers:$ID,"$PR_ID",--input, the full fix(source-control): confirm a merge-queue PR entered the queue after gh pr merge #7075 shape, an unreadable literal node)--help/-hstay-quiet cases that assert no gh callgh pr -R o/r mergetimeoutbinary on PATH (returns in under 8 s), run on a copy with a 2 s budgetGUARDRAILS_BUPM_BUDGET_S=1with a 0.7 s gh still blocks). It failed before the knob was removed.A fake
ghon PATH serves fixtures and logs calls. Covers every merge path, both shells, MCP, GraphQL-refused fallback to the cloud route, current-branch lookup, the fail-open cases (gh missing, 401, no thread source, no node id), and MUST-stay-quiet cases that also assert noghcall (commit message, echo,gh pr view,--disable-auto, GET of the merge route, a GraphQL query, grep,git merge, heredoc, PowerShell quoted mention, an unrelated MCP tool). Also runs throughrun-guards.sh.scripts/affected-tests.sh --base origin/main --run:run-guards.test.sh's declaration checks for the new guard. The 2 selected.mjssuites, run separately withnode --test, also passed.check-killswitch-hoist28/28,check-script-contract51/51,node-notice-rows6/6,check-guardrails-ps-differential21/21.block-hook-bypass, which fails identically onorigin/mainwhen the checkout sits under/tmp. A full affected run on this head is still to do.shellcheck -x,check-killswitch-hoist,check-changelog-fragments --check-required origin/mainandcheck-changelog-parity --checkpass.Live, read-only, in a Claude Code cloud session: the guard against open PRs fix(planning): word interview questions so yes accepts the recommendation #7036 (blocked: failing
ci-status,ci-lanes), chore(release): release 27 plugins #6910 (blocked: pending checks), feat: adopt the 2,000-PRs talk decisions across the marketplace #7009 (allowed); 3.0 to 3.6 s per merge command (five sequential calls), recorded in the guard header and README.shellcheck -x(repo.shellcheckrc) clean;scripts/validate-plugins.sh,scripts/check-changelog-fragments.sh --check-required origin/main,scripts/check-changelog-parity.sh --check,scripts/check-killswitch-hoist.sh, markdownlint on the README and fragment: all pass.This changes what agents may do unattended (it blocks merges), so it needs a human merge decision.
Related
/source-control:babysit-prsmerge gate counts threads the same way (outdated included)🤖 Generated with Claude Code
https://claude.ai/code/session_01L8KdG7fWg2YrErVYWtohmR
Generated by Claude Code
Generated by Claude Code