fix: BOT_REVIEW posted nothing when /code-review wrote its POST flag-first - #4773
Conversation
…first The scoped grant for the review and cleanup flows was a single prefix glob, `Bash(gh api repos/springfall2008/batpred/*)`. Permission rules match a literal command prefix, so that only fires when the endpoint is the very next token, bare. `/code-review --comment` writes its inline comments as `gh api --method POST repos/...` - the canonical form for a POST - which misses the rule and is denied outright under dontAsk. PR #4758 is what this looked like in practice: the review ran, produced nine findings, had every posting call denied, printed the findings into the log instead, and still exited 0. `process_bot_review_pr` read that as success and removed BOT_REVIEW, so the PR merged with zero reviews and zero comments. #4759 posted fine the same evening purely because its POST happened to be endpoint-first. Verified against `claude -p --permission-mode dontAsk`: with the old allowlist the call is refused by the permission layer before it runs; with the new one it is permitted and reaches GitHub. Three changes: - Enumerate the realistic command spellings - {bare, --method POST/PATCH, -X POST/PATCH} x {bare, ", '}. Only POST and PATCH, and every variant stays pinned to this repo, so this widens the accepted spelling and not the reach. - Pin the form with --append-system-prompt on both flows holding the grant. /code-review is a built-in skill, so this cannot live in a SKILL.md we own. It also asks for a denial to be stated plainly rather than degraded into printing the comments that could not be posted. - Verify before labelling: pr_review_activity_count() samples reviews, inline comments and PR comments either side of the run. An unchanged count now means BOT_FAILED with a specific reason, because a zero exit status cannot show whether the posting step was permitted. 24 new tests covering the denied command forms, the repo pinning, the absence of DELETE/PUT, the appended prompt on both flows, the activity counter's pagination and empty-response handling, and the posted-nothing path. 108/108 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new activity-count verification introduces uncaught failure paths (and the appended prompt text is now misleading vs the expanded allowlist), which can leave BOT_REVIEW stuck/retried or confuse the agent.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR hardens the triage daemon’s BOT_REVIEW PR-review flow so /code-review … --comment reliably has permission to post its findings (regardless of gh api flag ordering / quoting), and so the bot doesn’t clear BOT_REVIEW when nothing was actually posted.
Changes:
- Expand the scoped
gh apiallowlist to cover realistic POST/PATCH spellings (endpoint-first, method-flag-first, and quoted endpoint variants) while staying pinned tospringfall2008/batpred. - Append a system prompt to
/code-reviewand/pr-cleanupruns to steer the agent toward the endpoint-firstgh apiform and to require explicit reporting on permission denials. - Add
pr_review_activity_count()and use it inprocess_bot_review_pr()to detect “exited 0 but posted nothing” and mark such runs asBOT_FAILEDinstead of removingBOT_REVIEW.
File summaries
| File | Description |
|---|---|
| tools/triage_daemon.py | Broadens the scoped gh api allowlist, adds an appended prompt to guide gh api usage, and verifies posted activity before clearing BOT_REVIEW. |
| tools/test_triage_daemon.py | Adds regression tests covering the expanded allowlist forms, the appended prompt, and the new activity-count verification logic. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
The current repo-scoped gh api allow patterns still permit destructive verbs when flags are placed after the endpoint, and the related test asserts a guarantee that the permission model does not actually enforce.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
tools/test_triage_daemon.py:491
test_scoped_gh_api_grants_never_allow_destructive_methodscurrently checks that the allowed tool patterns don’t contain the strings "DELETE"/"PUT", but the endpoint-first patterns still allow a caller to append-X DELETE/--method PUTafter the endpoint (so the absence of those substrings in the allowlist doesn’t actually prove the verbs are blocked).
If you add explicit destructive-method denials (recommended), the test should assert those denials are present in DISALLOWED_TOOLS_REVIEW/DISALLOWED_TOOLS_CLEANUP instead.
"""Only POST and PATCH are enumerated - the review flow creates and edits comments,
it never needs DELETE or PUT, and spelling those out would hand it the REST routes to
remove reviews or replace branch contents."""
for flow in [triage_daemon.ALLOWED_TOOLS_REVIEW, triage_daemon.ALLOWED_TOOLS_CLEANUP]:
for entry in flow.split(","):
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
| _GH_API_METHOD_FLAGS = ["", "--method POST ", "--method PATCH ", "-X POST ", "-X PATCH "] | ||
| _GH_API_ENDPOINT_QUOTES = ["", '"', "'"] | ||
| _REVIEW_EXTRA_ALLOWED = [f"Bash(gh api {flag}{quote}repos/{REPO}/*)" for flag in _GH_API_METHOD_FLAGS for quote in _GH_API_ENDPOINT_QUOTES] | ||
| ALLOWED_TOOLS_REVIEW = ",".join(_ALLOWED_GH_PR_READ + _ALLOWED_TOOLS_NON_GH + _REVIEW_EXTRA_ALLOWED) | ||
| DISALLOWED_TOOLS_REVIEW = ",".join(item for item in _DISALLOWED_TOOLS_BASE if item not in _REVIEW_REMOVED_DENIALS) |
The bug
The triage bot's
BOT_REVIEW-on-PR flow ran, produced findings, and posted none of them — then cleared its own label as if it had succeeded.PR #4758is what that looked like:/code-review 4758 high --commentran to completion, produced nine findings, had every posting call denied, printed the findings intologs/pr-4758-review.loginstead, and exited 0.process_bot_review_prread the zero exit as success and removedBOT_REVIEW. The PR merged with"reviews": 0, "comments": 0, "labels": [].Root cause
The scoped grant for the review and cleanup flows was a single prefix glob:
Permission rules match a literal command prefix, so this only fires when the endpoint is the very next token, bare.
/code-review --commentwrites its inline comments asgh api --method POST repos/...— the canonical form for a POST — which misses the rule and is denied outright underdontAsk.PR #4759posted successfully the same evening purely because its POST happened to be endpoint-first. Which of the two reviews landed came down to the command spelling the model picked.The existing comment above the rule had already predicted this: "Prefix-glob matching can't parse flags, so this is a heuristic, not a guarantee."
Verification
Ran
claude -p --permission-mode dontAskagainst both allowlists with the same command, using a deliberately non-existent endpoint so a permitted call creates nothing:gh api --method POST repos/springfall2008/batpred/…Also confirmed from the session transcripts that a quoted endpoint (
gh api "repos/...") is denied for the same reason, and that redirecting output outside the clone or scratch directory is denied while piping into an allowlisted reader is fine.The fix
Enumerate the realistic command spellings —
{bare, --method POST/PATCH, -X POST/PATCH} × {bare, ", '}. Only POST and PATCH (the flow creates and edits comments; it never needs DELETE or PUT), and every variant stays pinned to this repo — this widens the accepted spelling, not the reach.Pin the form with
--append-system-prompton both flows holding the grant./code-reviewis a built-in skill, so this can't live in a SKILL.md we own. It also asks for a denial to be stated plainly rather than quietly degraded into printing the comments that couldn't be posted.Verify before labelling —
pr_review_activity_count()samples reviews, inline comments and PR comments either side of the run. An unchanged count now meansBOT_FAILEDwith a specific reason, because a zero exit status cannot show whether the posting step was permitted.The first two are belt and braces: the allowlist covers the spellings we enumerated, the prompt keeps the agent on the one that's certain to be covered.
Tests
24 new tests, written before the fix and confirmed failing for the right reasons — the denied command forms, the repo pinning, the absence of DELETE/PUT, the appended prompt on both flows, the activity counter's pagination and empty-response handling, and the posted-nothing path.
108/108 pass.
pre-commitclean on both files (ruff, black, cspell, the daemon test hook).Note for deploying
The running daemon won't pick this up on its own —
sync_repo()resets the clone on disk, but the already-loadedtriage_daemonmodule stays as it was. It needs a restart after this merges.🤖 Generated with Claude Code