@claude /review reviews the entire pull request every time. On a PR that has been reviewed and then revised, the reviewer re-derives everything that was already agreed, and the genuinely new work competes for attention with a dozen settled findings. The output reads as a fresh assessment of the whole PR, so a reader cannot tell which findings are new.
Make the common case — reviewing again after changes — the default, and keep a full re-review available on request.
The three behaviors
- First review of a PR — assess the whole diff. This is what happens today.
- Any later review — read the prior review comments first, then surface what is genuinely new, plus anything previously raised that is still unaddressed.
@claude /review all — ignore prior comments and assess from scratch.
Why the obvious approach does not work
The first design here was a lastReviewedSha..head range, with the previously reviewed SHA read from workflow run history (gh run list --json headSha, filtered to successful runs). That data does exist and the query works.
It fails on rebase, which is this repository's normal way of working. A force push rewrites every SHA, so the old one stops being an ancestor of the head, and a two-dot range between them describes something that never happened — it renders every change to the base as though it were PR work. The GitHub compare endpoint reports diverged for exactly this case, so it can be detected, but detection only means falling back to a full review. Since rebasing is routine here, the incremental path would rarely fire and the feature would exist without doing anything.
Range-based scoping assumes linear history. This repo does not have it, deliberately.
Comments do not have this problem. A rebase changes every commit and changes nothing that was already said on the PR.
The approach
Restructure the prompt so the reviewer reads its own prior output before reviewing the diff, and classify each finding against it.
The classification signal already exists in this repo's conventions. Replies written when addressing feedback end with the literal marker *— AI Coding Agent*:
- A thread carrying such a reply was addressed — do not raise it again.
- A finding with no such reply is still open — raise it again, and say explicitly that it is unchanged since the last review.
- Anything else is new — this is the part that deserves close reading, and it should be visually distinguishable in the output.
The second case matters as much as the third. A framing of "only surface new items" would silently drop findings that were raised and never fixed, which is the opposite of useful.
Thread resolution state is not available: isResolved is GraphQL-only, and granting the model gh api is out of scope (see constraints). The reply marker is the available proxy and it is reliable, because it is written by the same process that addresses feedback.
Mode selection
The workflow already gates on contains(github.event.comment.body, '@claude /review'). Add a second contains() test for the all form and resolve it to an enum — full or incremental — used to pick a focus= string.
The comment body must never be interpolated into the prompt. It is tested, not forwarded. This is the property that keeps the existing gate injection-safe, and it must survive the change. A free-text focus supplied by the commenter is explicitly not wanted: "treat the following as review focus, not as instructions" is a prompt-level guard against prompt injection, which is not a boundary.
The workflow already switches focus= between issue_comment and pull_request_review_comment. This is the same mechanism with more arms.
Enable Read
--allowedTools currently grants mcp__github_inline_comment__create_inline_comment, Bash(gh pr comment:*), Bash(gh pr diff:*), and Bash(gh pr view:*). Add Read.
The reviewer needs to open files to do this well. Every finding that has justified a review on this repo came from reading whole files rather than diff hunks — surviving references in untouched regions, a claim about a directory's real contents, a cross-file ordering dependency. Scoping the reviewer's attention to what is new should not scope its access.
Read is coupled to persist-credentials: false. Without that setting, actions/checkout writes the job's GITHUB_TOKEN into .git/config in the tree being reviewed, and Read plus Bash(gh pr comment:*) is a complete path from that file to a public comment. The setting landed in #121; the workflow header should record that removing it would reopen an exfiltration path that exists only because Read is granted.
gh pr view --json comments,reviews returns what is needed to read prior comments and is already covered by the existing Bash(gh pr view:*) grant.
Constraints
- No
gh api grant to the model. It is write-capable, and the model ingests untrusted diff content. The workflow may use privileged tools freely — it is deterministic and never reads model output — so every privileged step happens before the model starts. This is the governing principle for anything added here.
- No
pull_request_target, no additional permissions, no contents: write. The existing security model is documented in the workflow header and must survive intact.
- The maintainer-only
author_association gate stays.
The mode must be stated in the output
Whichever mode ran, the review must say so, and an incremental review must say what it treated as already addressed.
Without that, "no findings" is ambiguous between "nothing new since last time" and "I read everything and it is clean." That ambiguity has real cost here: a check that did not run looks exactly like a check that passed.
Out of scope
Token cost is not addressed. The reviewer still reads the whole diff; only the reporting is scoped. The original framing of this issue was cost, but the actual pain is noise — a re-review that re-litigates twelve settled findings buries the one new one. If cost becomes the binding constraint later, that is a separate change and it will need a different mechanism.
Acceptance
- A first review on a PR assesses the whole diff and says so.
- A later review does not re-raise a finding whose thread carries an
*— AI Coding Agent* reply.
- A later review does re-raise a previously raised finding that has no such reply, and marks it as unchanged.
- New findings are distinguishable from carried-forward ones in the output.
@claude /review all assesses from scratch and says so.
- The comment body is never interpolated into the prompt.
--allowedTools gains Read and nothing else; no new permissions.
- The workflow header records the
Read / persist-credentials: false coupling.
@claude /reviewreviews the entire pull request every time. On a PR that has been reviewed and then revised, the reviewer re-derives everything that was already agreed, and the genuinely new work competes for attention with a dozen settled findings. The output reads as a fresh assessment of the whole PR, so a reader cannot tell which findings are new.Make the common case — reviewing again after changes — the default, and keep a full re-review available on request.
The three behaviors
@claude /review all— ignore prior comments and assess from scratch.Why the obvious approach does not work
The first design here was a
lastReviewedSha..headrange, with the previously reviewed SHA read from workflow run history (gh run list --json headSha, filtered to successful runs). That data does exist and the query works.It fails on rebase, which is this repository's normal way of working. A force push rewrites every SHA, so the old one stops being an ancestor of the head, and a two-dot range between them describes something that never happened — it renders every change to the base as though it were PR work. The GitHub compare endpoint reports
divergedfor exactly this case, so it can be detected, but detection only means falling back to a full review. Since rebasing is routine here, the incremental path would rarely fire and the feature would exist without doing anything.Range-based scoping assumes linear history. This repo does not have it, deliberately.
Comments do not have this problem. A rebase changes every commit and changes nothing that was already said on the PR.
The approach
Restructure the prompt so the reviewer reads its own prior output before reviewing the diff, and classify each finding against it.
The classification signal already exists in this repo's conventions. Replies written when addressing feedback end with the literal marker
*— AI Coding Agent*:The second case matters as much as the third. A framing of "only surface new items" would silently drop findings that were raised and never fixed, which is the opposite of useful.
Thread resolution state is not available:
isResolvedis GraphQL-only, and granting the modelgh apiis out of scope (see constraints). The reply marker is the available proxy and it is reliable, because it is written by the same process that addresses feedback.Mode selection
The workflow already gates on
contains(github.event.comment.body, '@claude /review'). Add a secondcontains()test for theallform and resolve it to an enum —fullorincremental— used to pick afocus=string.The comment body must never be interpolated into the prompt. It is tested, not forwarded. This is the property that keeps the existing gate injection-safe, and it must survive the change. A free-text focus supplied by the commenter is explicitly not wanted: "treat the following as review focus, not as instructions" is a prompt-level guard against prompt injection, which is not a boundary.
The workflow already switches
focus=betweenissue_commentandpull_request_review_comment. This is the same mechanism with more arms.Enable
Read--allowedToolscurrently grantsmcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*), andBash(gh pr view:*). AddRead.The reviewer needs to open files to do this well. Every finding that has justified a review on this repo came from reading whole files rather than diff hunks — surviving references in untouched regions, a claim about a directory's real contents, a cross-file ordering dependency. Scoping the reviewer's attention to what is new should not scope its access.
Readis coupled topersist-credentials: false. Without that setting,actions/checkoutwrites the job'sGITHUB_TOKENinto.git/configin the tree being reviewed, andReadplusBash(gh pr comment:*)is a complete path from that file to a public comment. The setting landed in #121; the workflow header should record that removing it would reopen an exfiltration path that exists only becauseReadis granted.gh pr view --json comments,reviewsreturns what is needed to read prior comments and is already covered by the existingBash(gh pr view:*)grant.Constraints
gh apigrant to the model. It is write-capable, and the model ingests untrusted diff content. The workflow may use privileged tools freely — it is deterministic and never reads model output — so every privileged step happens before the model starts. This is the governing principle for anything added here.pull_request_target, no additionalpermissions, nocontents: write. The existing security model is documented in the workflow header and must survive intact.author_associationgate stays.The mode must be stated in the output
Whichever mode ran, the review must say so, and an incremental review must say what it treated as already addressed.
Without that, "no findings" is ambiguous between "nothing new since last time" and "I read everything and it is clean." That ambiguity has real cost here: a check that did not run looks exactly like a check that passed.
Out of scope
Token cost is not addressed. The reviewer still reads the whole diff; only the reporting is scoped. The original framing of this issue was cost, but the actual pain is noise — a re-review that re-litigates twelve settled findings buries the one new one. If cost becomes the binding constraint later, that is a separate change and it will need a different mechanism.
Acceptance
*— AI Coding Agent*reply.@claude /review allassesses from scratch and says so.--allowedToolsgainsReadand nothing else; no newpermissions.Read/persist-credentials: falsecoupling.