Fix claude-code-review republishing a raw gh pr comment as the review body - #318
Conversation
… body When the review agent posts its own summary via a `gh pr comment N --body "$(cat <<EOF ... EOF)"` Bash call — a trusted review-text candidate when permission_denials_count is 0 — check-review-execution.sh used the whole command string as the posted review text. The "Claude finished review" summary comment then rendered a literal `gh pr comment ... <<EOF ...` block, so the review appeared twice on the PR: once correctly (the agent's own post) and once mangled (this wrapper's). Unwrap the heredoc body from such a command before using it as the posted text; fall back to the command unchanged when there is no heredoc (a --body/--body-file form the pass/fail scan can still read a verdict from). The single quote in the heredoc-delimiter match is written as the \x27 hex escape so the jq program carries no literal quote to collide with the surrounding shell single-quoting, and capture() is wrapped in [...] | first because it yields empty (not null) on no match, which would otherwise annihilate the pipeline. Both review_text_file and all_text_file still derive from the same unwrapped block, preserving the gha#218 finding-2 invariant that the posted text and the pass/fail scan never diverge. Adds fixture verdict-via-gh-comment-heredoc.json (a denials=0 heredoc post plus a trailing no-verdict narration block) asserting the posted text is the unwrapped review and does NOT contain `gh pr comment`. Seen on UCD-SERG/serocalculator#614. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 🚀 New features to boost your workflow:
|
Code reviewReviewed the diff,
Everything else checks out: the VerdictNeeds more work — the regex-truncation bug (finding 1) should be fixed, and ideally pinned with a fixture where the review body itself contains a line starting with the heredoc tag, before merge. |
|
Claude finished review — View run Review posted. Summary: PR #318 fixes a real bug where
Both inline comments and a top-level summary comment with the verdict were posted to the PR. VerdictNeeds more work — finding 1 should be fixed before merge. |
|
Working on this — paws off until I'm done. Generated by Claude Code |
Addresses both findings from the review on #318. Finding 1 (blocking): the heredoc-unwrapping regex ended the body at `\n[ \t]*\k<tag>\b`, which has no end-of-line anchor -- a review body line merely starting with the tag satisfied it, and the lazy body then stopped there, truncating the posted review. Reproduced with jq: a body containing `EOF markers must stand alone...` was cut to its first heading, dropping the verdict (which then also failed the guard's verdict scan, turning a genuine review into a stub failure). The reviewer's suggested anchor alone does not close this: their own example -- an indented ` EOF` inside a quoted shell snippet -- still terminates early, because the leading `[ \t]*` accepts a space-indented terminator that real bash `<<EOF` does not. Rather than stack more anchors on the regex, the regex now finds only the opener and the terminator is located by comparing whole lines against the tag, the way bash itself ends a heredoc. `<<-`'s leading-tab stripping is mirrored too, so a tab-indented heredoc posts de-indented markdown instead of a code block. Two regression fixtures added, both verified to fail against the pre-fix script and pass against this one: a body carrying tag-prefixed lines and an indented terminator lookalike, and a `<<-` tab-indented form. Finding 2 (non-blocking): the CHANGELOG.md entry moves to `changelog.d/fix-review-heredoc-republish.fixed.md`, per CLAUDE.md's fragment convention. Also merges current `main` into the branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6RX6sDPcTTfRMBfY3ZSHo
ARD round 1 — both findings addressed (
|
| # | Finding | Disposition |
|---|---|---|
| 1 | Heredoc terminator has no end-of-line anchor, truncating the posted review | Addressed (fixed differently than suggested — see below) |
| 2 | Changelog entry hand-added to CHANGELOG.md instead of a changelog.d/ fragment |
Addressed |
Finding 1 — confirmed, and the suggested fix was insufficient
The bug is real and reproduced before touching anything. It's also a bit worse than described: truncating the body drops the verdict, which then fails the guard's own verdict scan — so the pre-fix code can turn a genuine review into a stub failure (exit=1), not just a mangled comment.
But the suggested \k<tag>[ \t]*(?:\r?\n|$) replacement, tested against the same harness, does not fix the indented EOF case from the review comment's own example. The terminator's leading [ \t]* still accepts a space-indented closing line that real bash <<EOF rejects, so the body still ends at body.
Rather than add another anchor, the regex now matches only the heredoc opener and the terminator is found by comparing whole lines against the tag — how bash itself ends a heredoc. That removes the lazy-quantifier/anchor failure mode outright instead of narrowing it, and drops the \k<tag> backreference from an already heavily-escaped jq-inside-shell string. <<-'s leading-tab stripping is mirrored too, so a tab-indented heredoc posts de-indented markdown instead of an accidental code block. With no terminator line at all (transcript truncated mid-command) it falls back to the raw command — posting a shell-looking comment beats dropping review text.
Verification
Two fixtures added, each checked in both directions — failing against the pre-fix script, passing against this one:
| Fixture | Covers | Pre-fix |
|---|---|---|
...-tag-in-body.json |
body line starting with EOF, plus the indented EOF lookalike, verdict below both |
exit=1, verdict truncated away |
...-dash-tab.json |
<<-'EOF' with tab-indented body/terminator |
passes but posts tab-indented text |
All 16 fixtures pass. main was also merged in (it had advanced past this branch with #317).
Note
Per the "can't self-verify" section of CLAUDE.md, this PR edits check-review-execution.sh, which reaches this repo's own review only through @v2 — so every review of this PR runs the pre-fix guard. The fixture suite is the real evidence here, not the review check's own behavior.
Generated by Claude Code
|
Claude finished review — View run Code review — #318I re-reviewed the current head commit ( Finding 1 (blocking, tactical correctness bug) — fixed, and independently verified. The new commit replaces the inline-regex terminator entirely: the regex now matches only the heredoc opener, and the terminator is located by splitting the body into lines and finding the first line that is exactly the tag (mirroring real bash
This also correctly handles the case the previous review's own suggested fix ( Finding 2 (non-blocking, process nit) — fixed. The Minor observation, not blocking: in the new No other issues found: the test wiring in VerdictReady for merge — both findings from the prior review round are fixed and independently verified by executing the actual jq logic against the new fixtures and synthetic edge cases; no new blocking issues found. |
…ator match
Round-2 review observation (non-blocking): rtrimstr("\r") was applied only
to the array used to locate the terminator, so a CRLF transcript still
left stray carriage returns in the array that gets sliced and posted.
Normalize once, before both uses.
Adds verdict-via-gh-comment-heredoc-crlf.json, verified to fail against
the round-1 code and pass against this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6RX6sDPcTTfRMBfY3ZSHo
ARD round 2 — the non-blocking observation is addressed too (
|
|
Code review Confirming review — re-checked the current head commit ( The prior review gave a verdict of Ready for merge, with both blocking findings from round 1 (the heredoc-terminator regex's missing end-of-line anchor, and the This new commit fixes exactly that: I hand-traced this fixture's bytes through the updated jq function line by line ( One review agent raised a possible test-coverage gap (no fixture exercises the No new issues found. Verdict: Ready for merge — the CRLF fix in |
|
Claude finished review — View run The confirming review is posted correctly: comment link. SummaryReviewed PR #318 at its current head commit ( I independently verified the fix by hand-tracing the updated VerdictReady for merge — no new issues found; the CRLF fix is correct and independently verified. |
|
Confirming the all-clear now that CI has actually finished — the round-3 verdict posted while several jobs were still in flight, so this is the state at completion rather than at verdict time. At head
On the coverage gap one of your review agents raised and then rejected (no fixture for the Ready for merge — leaving the merge itself to you. Generated by Claude Code |
) Two learnings from driving Morrison-Lab/gha#318 (heredoc-unwrap fix) through three review rounds to merge. memories/git.md: a new section on picking the diff range when self-checking a PR. Both wrong choices hit in one session -- `origin/main..HEAD` (two dots) rendered a sibling PR's already-merged file as a deletion by this PR, nearly reported as a finding; and `origin/main...HEAD` reported an em-dash scan clean because the edits were still uncommitted. Cross-references the existing diff-scoped-no-op section rather than restating it, and adds the worktree-comparing `git diff origin/main` as a second fix for the by-hand case. shared/workflow/address-every-comment.md: extends the verify-the-suggestion bullet to a fix a reviewer describes in prose. A finding that ships a repro case has handed you a test fixture -- run the proposed fix against that case before adopting it. On #318 the suggested regex anchor was directionally right but still truncated the reviewer's own cited example. Claude-Session: https://claude.ai/code/session_01K6RX6sDPcTTfRMBfY3ZSHo Co-authored-by: claude <noreply@anthropic.com>
The reviewer agent self-posted its `## Code review` summary via `gh pr comment` (as claude[bot]) while the workflow's "Post review comment" step also posted it (as github-actions[bot]), so every review round left two top-level comments. Three places claimed `gh pr comment` was "not granted / denied", but it was only absent from the allowlist -- the code-review plugin's command frontmatter re-granted it, and nothing in --disallowedTools took it away. - Deny `Bash(gh pr comment:*)` in run-claude-review-attempt's --disallowedTools, and reword the reviewer prompt to OUTPUT its review (findings + Verdict) as its final message instead of posting a top-level comment. The workflow posts it, with run-link header, collapse, and cost linkage the self-post lacked. - Switch the prior-review-context fetch and the collapse step to match the github-actions[bot] author the workflow posts under (both admit claude[bot] too, for tag-mode tracking comments). The collapse step previously matched claude[bot] alone, so it silently folded nothing in agent mode. - Correct the stale "not granted / necessarily denied" comments in the composite header and check-review-execution.sh. Retires the raw-gh-pr-comment-republishing class (#312, #318, #381): with the tool denied the agent never issues the command, so there is nothing to republish. Closes #381. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…g the review (#400) * start: deny reviewer gh pr comment to stop duplicate review comments (closes #381) * fix(review): deny reviewer gh pr comment so it stops duplicate-posting The reviewer agent self-posted its `## Code review` summary via `gh pr comment` (as claude[bot]) while the workflow's "Post review comment" step also posted it (as github-actions[bot]), so every review round left two top-level comments. Three places claimed `gh pr comment` was "not granted / denied", but it was only absent from the allowlist -- the code-review plugin's command frontmatter re-granted it, and nothing in --disallowedTools took it away. - Deny `Bash(gh pr comment:*)` in run-claude-review-attempt's --disallowedTools, and reword the reviewer prompt to OUTPUT its review (findings + Verdict) as its final message instead of posting a top-level comment. The workflow posts it, with run-link header, collapse, and cost linkage the self-post lacked. - Switch the prior-review-context fetch and the collapse step to match the github-actions[bot] author the workflow posts under (both admit claude[bot] too, for tag-mode tracking comments). The collapse step previously matched claude[bot] alone, so it silently folded nothing in agent mode. - Correct the stale "not granted / necessarily denied" comments in the composite header and check-review-execution.sh. Retires the raw-gh-pr-comment-republishing class (#312, #318, #381): with the tool denied the agent never issues the command, so there is nothing to republish. Closes #381. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Problem
On
UCD-SERG/serocalculator#614, the "Claude finished review" summary comment posted a raw, unexecuted shell command as its body — agh pr comment N --body "$(cat <<HEREDOC ... HEREDOC)"invocation, shown literally instead of run.The review itself posted fine as a separate
claude[bot]comment, so the content appeared twice — once correctly, once as a mangledgh pr comment ... <<HEREDOC ...block.Root cause
check-review-execution.shbuilds its review-text candidates from assistant blocks. Agh pr comment/gh api .../commentsBash call is a deliberate candidate (so a verdict the agent posted directly, but never restated in plain text, is still recognized) — but only whenpermission_denials_count == 0, since those calls are otherwise denied (gha#218 finding 1).The bug: when that candidate was selected, the code used the whole command string as the review text. That string feeds two consumers — the pass/fail verdict scan (fine, the verdict is inside the heredoc) and
review_text_file, which is posted verbatim. So the wrapper republished the command.It's intermittent, which is why prior rounds looked clean: it only triggers when the agent's final action is a
gh pr commentself-post rather than plain narration or the inline-comment MCP tool.Fix
Unwrap the heredoc body from such a command before using it as the posted text; fall back to the command unchanged when there is no heredoc (a
--body/--body-fileform the scan still reads a verdict from). Two implementation notes, both load-bearing:\x27hex escape, so the jq program carries no literal quote to collide with the surrounding shell single-quoting.capture()is wrapped in[...] | first, because it yields empty (not null) on no match — which would annihilate the whole pipeline. (Hit this in a first draft; the block came out empty.)review_text_fileandall_text_filestill derive from the same unwrapped block, so the gha#218 finding-2 invariant (posted text and scanned text never diverge) holds.Test
Adds fixture
verdict-via-gh-comment-heredoc.json— adenials=0heredoc self-post plus a trailing no-verdict narration block — wired intorun-fixture-tests.shwith:expected = passmust_contain = 'One real finding on line 12'(the unwrapped review)must_not_contain = 'gh pr comment'(proves the command string is no longer posted)Parallels the existing
verdict-via-inline-comment-tool.json, which asserts the same posted-vs-scanned distinction for the other posting path.Verification: macOS ships bash 3.2, so the
declare -Arunner can't execute locally — I drove the realcheck-review-execution.shdirectly against the new fixture and the neighbours (genuine-finished-review,verdict-via-inline-comment-tool,denied-bash-comment-not-trusted,empty-review-text), confirming the new case posts the unwrapped body and every existing case still behaves. The fulldeclare -Asuite runs on CI (Ubuntu / bash 5).🤖 Generated with Claude Code