A Claude review that posted nothing no longer finishes green: read-only tools end the denial churn, the job fails when no verdict was submitted, and the transcript survives the runner - #3657
Merged
Conversation
…ly tools end the denial churn, the job fails when no verdict was submitted, and the transcript survives the runner (#3650) The prompt told the reviewer the branch was checked out and asked for a correctness, parity and security review, while --allowedTools permitted only four gh verbs and the inline-comment tool. Every Read, Grep, Glob and git call was a permission denial; the swallowed runs' result blocks read 50 turns / 18 denials, 39 / 21, 18 / 17, each ending subtype=success with nothing posted. Three PRs, about thirteen paid runs in one night. - --allowedTools gains Read, Grep, Glob, Bash(git diff:*), Bash(git log:*), Bash(git show:*). Nothing that writes. - A step after the action fails the job when claude[bot] submitted no non-empty-bodied review since the run's own start stamp, and prints the transcript's result block. A refusal-to-run (this file differing from the default branch's copy) is named separately. - claude-execution-output.json is uploaded as the claude-review-transcript artifact, 7 days. Lands on main and dev together: claude-code-action refuses to run whenever this file differs from the default branch's copy, so a dev-only edit would disable review on every PR until the next release.
This was referenced Sep 18, 2026
erikdarlingdata
marked this pull request as draft
September 18, 2026 22:49
erikdarlingdata
marked this pull request as ready for review
September 18, 2026 23:19
erikdarlingdata
added a commit
that referenced
this pull request
Sep 19, 2026
…spliced once Fifty-nine PRs merged to dev today across the coordinator's lanes and the wave-2 worker's; each lane returned its entry to a buffer instead of touching this file, so that fifty-plus PRs did not each rebase the same twenty lines. This is the one splice. Every entry is one line (the archiver's compact() and the pins read them that way); riders fold into their parent's entry (#3599 under #3590, #3619 under #3611, #3623 under #3616, #3640 under #3633; #3617 test-only and #3661 re-cut as #3666 carry none); #3657's entry is in because it MERGED to dev - the twin to main is what is still pending. [Unreleased] gains a `### Added` above `### Fixed` (Keep-a-Changelog order) for the six new capabilities: per-user theme colours (#3606 / #3577 arm B), routed alert families (#3668 / #3598), the PostgreSQL logging audit tool (#3643 / #3607), the service-side wait sampler (#3645 / #3604), and the log-event classifier with its temp-file / autovacuum parser families (#3646 / #3601, #3664 / #3602 #3603). The other forty-eight are honesty fixes to existing surfaces and append to `### Fixed` after the wave-1 bullets, in PR-number order. Thirty-six reference definitions added for the issues the new entries cite and the index did not yet define; the [Unreleased] group is one ascending run again, which moves [#3557] into its slot (the one deleted line). Nothing under ## [3.8.0] or older is touched; the archive script was not run. One editorial touch: the #3585 entry ended in a dangling "Darling" and now reads "Darling only." (the tool exists only in the Darling MCP host). tools/changelog/changelog_archive.py verify: all PASS (1420 bold entries, floor 1,329; 1,358 distinct refs resolve; CRLF throughout; 356,539 bytes under the 750 KiB ceiling). ChangelogIndexAndArchiveTests: 5/5 pass via a net10.0 harness.
This was referenced Sep 19, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 19, 2026
…spliced once (#3672) Fifty-nine PRs merged to dev today across the coordinator's lanes and the wave-2 worker's; each lane returned its entry to a buffer instead of touching this file, so that fifty-plus PRs did not each rebase the same twenty lines. This is the one splice. Every entry is one line (the archiver's compact() and the pins read them that way); riders fold into their parent's entry (#3599 under #3590, #3619 under #3611, #3623 under #3616, #3640 under #3633; #3617 test-only and #3661 re-cut as #3666 carry none); #3657's entry is in because it MERGED to dev - the twin to main is what is still pending. [Unreleased] gains a `### Added` above `### Fixed` (Keep-a-Changelog order) for the six new capabilities: per-user theme colours (#3606 / #3577 arm B), routed alert families (#3668 / #3598), the PostgreSQL logging audit tool (#3643 / #3607), the service-side wait sampler (#3645 / #3604), and the log-event classifier with its temp-file / autovacuum parser families (#3646 / #3601, #3664 / #3602 #3603). The other forty-eight are honesty fixes to existing surfaces and append to `### Fixed` after the wave-1 bullets, in PR-number order. Thirty-six reference definitions added for the issues the new entries cite and the index did not yet define; the [Unreleased] group is one ascending run again, which moves [#3557] into its slot (the one deleted line). Nothing under ## [3.8.0] or older is touched; the archive script was not run. One editorial touch: the #3585 entry ended in a dangling "Darling" and now reads "Darling only." (the tool exists only in the Darling MCP host). tools/changelog/changelog_archive.py verify: all PASS (1420 bold entries, floor 1,329; 1,358 distinct refs resolve; CRLF throughout; 356,539 bytes under the 750 KiB ceiling). ChangelogIndexAndArchiveTests: 5/5 pass via a net10.0 harness.
erikdarlingdata
added a commit
that referenced
this pull request
Sep 19, 2026
… never ran and the PR did not touch claude-review.yml, the drift arm had one verdict, "review disabled repo-wide", for both a merged dev PR waiting on the next release and an unreviewed edit nobody read. A third arm reads the base branch's copy back to the merged PR that produced it and says "pending release" instead; the hostile arm is unchanged, and a lookup that dies there fails closed (#3673) PRs to main come only from dev (check-pr-branch.yml), so a fix to the review workflow cannot land on main and dev together outside a release: it merges to dev through a reviewed PR and every dev PR is skipped by claude-code-action until the release ships it. #3657 did exactly that tonight and every dev PR went red for it. The arm now asks provenance, not content: the newest commit on the base touching the file, the merged PR that introduced it, that PR's diff including the file, and the base's blob equal to the blob at its merge. All three hold: warning, exit 0, still a human's review. Any fails, or no PR claims the commit: the existing exit 1, text unchanged. Every read goes through lookup_closed(), a fail-CLOSED twin of the #2309 lookup(), because passing on a 503 would be the false verdict.
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.
The lie
A Claude review run that posted nothing — no comment, no inline note, no verdict — finished green. Tonight that happened on three PRs across roughly thirteen paid runs (#3618, #3642 ×6, #3646); the guard (#2229/#3492) caught each one after the fact, and each cost a run, a guard cycle and a human's time.
The mechanism (verified from the swallowed runs' own result blocks)
num_turns50 /permission_denials_count18, 39 / 21, 18 / 17 — denials ≈ turns, thensubtype: successand zero posts. The prompt says "The PR branch is already checked out in the working directory" and asks for a correctness / parity / security review, while--allowedToolspermitted only the inline-comment tool and fourgh prverbs — noRead,Grep,Glob, nogit. Every file open was a denial; after enough, the session ends without reaching the VERDICT PROTOCOL. Stochastic, so small diffs sometimes got lucky and a 37-file diff did not, five times on one head.The change — one file,
.github/workflows/claude-review.yml, byte-identical to #3656--allowedToolsgains read-only tools:Read,Grep,Glob,Bash(git diff:*),Bash(git log:*),Bash(git show:*). Nothing that writes.claude[bot]reviews withsubmitted_at >= stampand a non-empty body (the inline-comment tool's carrier reviews are bodiless — The interval-hourly refresh recomputes one fleet-wide bucket per dirty hour and paid twelve index inserts per row for eleven indexes nothing reads; the capture-down alert read decompressed a server's whole collection_log to learn two statuses (#3597, partial) #3647 has two of them — while both verdict shapes must carry a body). Zero →exit 1naming Claude review runs finish green having posted nothing: the allowlist forbids every read tool the prompt invites, denial churn ends sessions without a verdict, and the job never checks that it posted #3650/[CI] Review workflow can execute fully (real token spend) and post nothing - green check with vanished output #2229, result block in the log; "never ran" (this file ≠ default branch's copy) is named separately. Review step notsuccess→ notice, exit 0 (its own outcome is the story).gh apifailure → UNCONFIRMED warning, exit 0 per [CI] Review guard hard-fails on an unguarded lookup 404 — a lookup failure becoming the verdict its own comments forbid #2309, so a transient API error cannot force a paid re-run; the guard stays the enforcement. A clean review is never silent on this repo (the prompt mandates anLGTMreview), so zero is never legitimate and there is no false red.claude-execution-output.json→ artifactclaude-review-transcript, 7 days.Not changed: prompt, verdict protocol, model, the
Claude reviewstep name the guard couples to,fetch-depth, permissions.Landing — read #3656 first
claude-code-actionrefuses to run whenever a branch'sclaude-review.ymldiffers from the default branch's (main), so this file cannot land ondevalone without disabling review on every PR until the next release. #3656 lands the same commit onmainfirst; this PR lands the instant its required checks are green afterwards, restoringmain == devfor the file. Sequenced by hand — no auto-merge. Until #3656 merges, the guard here reads "Expected: this PR edits the workflow"; after it merges, this PR's copy equalsmain's and the guard passes on a re-run. The review itself refuses on this PR (cause 1 on the PR fixing it) and the new post-step reds the non-required review job with "Claude never ran" — expected; a human has reviewed the 150-line, one-file diff.Verified
actionlint+shellcheck clean;yaml.safe_loadok; sentinel and step name unchanged; the post-step's shell run locally against live PR data in six cases (details on #3656); no test pins reference the workflow.Closes #3650