fix(claude-code-review): don't let a denied tool call stub the review - #187
Conversation
check-review-execution.sh's stub-review guard (gha#172/#176) correctly fails a claude-review run that ends with no verdict, but the root cause of those stub runs was still open (gha#173, tracked further in gha#185): the reviewer's own fact-checking/hallucination-detection instructions can prompt it to attempt a WebFetch/WebSearch call that agent mode's default allowedTools doesn't grant, and on denial the agent sometimes stopped instead of finishing with a verdict. Reproduced 3/3 times on PR #180 with an identical permission_denials_count:1/num_turns:4 fingerprint. Tell the reviewer up front that network-fetch tools aren't available in this sandbox (so it fact-checks from the repo and its own knowledge instead of attempting the fetch) and that a denied tool call is never a reason to stop the review early. Closes #185.
|
Claude finished review — View run Review: PR #187 —
|
There was a problem hiding this comment.
Pull request overview
This PR hardens the claude-code-review reusable workflow against a flaky failure mode where a mid-review tool permission denial can derail the agent and result in a green run that posts only a stub (missing an explicit verdict), which downstream guard logic then correctly fails.
Changes:
- Extend
claude-code-review.yml’s appended system prompt to pre-declare thatWebFetch/WebSearchare unavailable and to require continuing the review after any denied tool call, finishing with an explicit verdict line. - Add a changelog fragment documenting the fix and its motivation/root cause (#185).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
.github/workflows/claude-code-review.yml |
Updates the reviewer’s system prompt to avoid derailing on denied tool calls and to set expectations about lack of network-fetch tools. |
changelog.d/review-continue-past-tool-denial.fixed.md |
Adds a Keep a Changelog–style fragment describing the fix for the stub-review failure mode. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…erdict (#201) * fix(claude-code-review): retry once when a review completes with no verdict Reopened #185 after a fresh reproduction post-#187: the low-denial stub-review signature (is_error:false, permission_denials_count:1, no verdict) still recurs even with #187's "don't stop on a denied tool call" system-prompt fix in place. check-review-execution.sh now surfaces this specific, retryable case (real non-empty text, no SDK error, no verdict) as a stub_review output, distinct from a hard SDK error or genuinely empty output. claude-code-review.yml retries the same review prompt once when it fires, with an added instruction that the retry must end with a verdict regardless of what gets denied along the way, before failing the check for real. The anthropics/claude-code-action call moved into a new run-claude-review-attempt composite action so the retry doesn't duplicate that ~100-line step. The raw execution output is now also uploaded as a workflow artifact on every attempt, so a future recurrence has a downloadable transcript to diagnose instead of needing show-full-output pre-enabled and a lucky re-trigger. Closes #185. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B2aHbEXwTUA13vYMuQXqas * fix(claude-code-review): gate stub-review retry on a denial-count threshold Review finding on #201: the retry's header comment and PR description claimed gha#198's high-denial-count no-verdict pattern was excluded from the retry, but check-review-execution.sh never actually checked permission_denials_count — stub_review fired purely on "no verdict", which is textually identical between #185 (denials: 1) and #198 (denials: 17-35). Add an actual permission_denials_count <= STUB_RETRY_MAX_DENIALS (default 5) gate, a fixture modeling #198's pattern expecting plain `fail` (not `fail-stub`), and correct every comment that claimed the exclusion without implementing it. Also fixes a second finding: "Resolve final review outcome"'s failure message always attributed a double-failure to attempt 1's stub_review flag, even when the retry failed for a different reason. It now inspects which attempt actually ran and why. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B2aHbEXwTUA13vYMuQXqas * refactor(claude-code-review): dedupe resolve+upload steps into a composite Review nit on #201: the attempt-1 and retry "Resolve execution file path"/"Upload review execution output" step pairs duplicated the same fallback-path shell logic and upload-artifact shape almost verbatim - the same class of duplication run-claude-review-attempt was extracted to avoid, just smaller. Folded both into a new upload-review-execution composite action. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B2aHbEXwTUA13vYMuQXqas --------- Co-authored-by: Claude <noreply@anthropic.com>
Root cause
#173(closed) tracked the symptom — aclaude-reviewrun going green with only a stub, no verdict — and#172/#174/#176added and tested a guard (check-review-execution.sh) that now correctly fails the check when this happens.#185is about the root cause that guard was correctly catching, which this PR fixes.Reproduced 3/3 times on PR #180 (a single-file
CLAUDE.mdprose addition) with an identical fingerprint:is_error: false,num_turns: 4,permission_denials_count: 1, no verdict written. The SDK call itself succeeded — this isn't the quota-exhaustion case (total_cost_usd==0 && num_turns==1).Job logs for those runs show
claude-code-actionlogs only theinit/finalresultmessages by default (show_full_output: falsehides everything in between "for security"), so the exact denied tool call isn't visible in the log — but the evidence converges on the same explanation:allowedToolsfor this run was["mcp__github_inline_comment__create_inline_comment", "Bash(python3:*)"](plus the disallowed git-write/python3 -c/-mlist) — noWebFetch/WebSearch. Per this file's own comments, agent mode's default allowlist covers only Read/Glob/Grep, comment-update + CI MCP tools, and git Bash.CLAUDE.md's own review guideline Smoke test: @claude agent workflow #4 (fact-check prose against external sources) and the review prompt's hallucination-detection instructions both tell the reviewer to verify claims "against the codebase and its dependencies" / an external source — but grant it no tool to do so.WebFetch-ing external URLs (raw.githubusercontent.com, a GitHub Pages doc site) — exactly the kind of content that would prompt the reviewer to attempt the denied call.Fix
Rather than widen the sandbox (granting broad
WebFetchaccess raises its own question — an agent with review-only secrets access fetching attacker-influenceable URLs from a diff is a prompt-injection/exfiltration surface worth deciding deliberately, not as a side effect of this bug fix), this PR targets the actual failure mode: the reviewer sometimes stops instead of finishing when a tool call is denied.claude-code-review.yml's--append-system-promptnow tells the reviewer up front that network-fetch tools aren't available in this sandbox (so it fact-checks from the repo's own content and its own knowledge instead of attempting the fetch, noting when something can't be independently verified) — and, more generally, that a denied tool call is never a reason to stop the review early; it must always finish with its findings and the explicit### Verdictline.Follow-up worth a separate decision
If live fact-checking against external docs (matching
CLAUDE.mdguideline #4) is wanted badly enough to justify the tradeoff, a follow-up could grant a domain-scopedWebFetch(domain:...)allowlist instead of leaving it fully disabled. Flagging rather than deciding here since it's a security-posture call for a workflow shared across every consumer repo, some of which may be private.Testing
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/claude-code-review.yml'))"— parses cleanly.changelog.d/README.md.Closes #185.
Generated by Claude Code