Skip to content

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
erikdarlingdata merged 1 commit into
devfrom
fix/3650-lane
Sep 18, 2026
Merged

erikdarlingdata merged 1 commit into
devfrom
fix/3650-lane

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

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_turns 50 / permission_denials_count 18, 39 / 21, 18 / 17 — denials ≈ turns, then subtype: success and 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 --allowedTools permitted only the inline-comment tool and four gh pr verbs — no Read, Grep, Glob, no git. 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

  1. --allowedTools gains read-only tools: Read, Grep, Glob, Bash(git diff:*), Bash(git log:*), Bash(git show:*). Nothing that writes.
  2. The job fails when it posted no verdict: a stamp before the action, and after it a count of claude[bot] reviews with submitted_at >= stamp and 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 1 naming 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 not success → notice, exit 0 (its own outcome is the story). gh api failure → 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 an LGTM review), so zero is never legitimate and there is no false red.
  3. Transcript retained: claude-execution-output.json → artifact claude-review-transcript, 7 days.

Not changed: prompt, verdict protocol, model, the Claude review step name the guard couples to, fetch-depth, permissions.

Landing — read #3656 first

claude-code-action refuses to run whenever a branch's claude-review.yml differs from the default branch's (main), so this file cannot land on dev alone without disabling review on every PR until the next release. #3656 lands the same commit on main first; this PR lands the instant its required checks are green afterwards, restoring main == dev for 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 equals main'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_load ok; 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

…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.
@erikdarlingdata
erikdarlingdata marked this pull request as draft September 18, 2026 22:49
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 18, 2026 23:19
@erikdarlingdata
erikdarlingdata merged commit 1dae2ad into dev Sep 18, 2026
15 of 19 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/3650-lane branch September 18, 2026 23:23
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.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant