claude-code-review.yml
Run a read-only Claude review of a pull request (the code-review plugin). By default, callers run it only on dispatched runs (for @claude review via claude.yml), which posts a consolidated summary. If the caller also enables a pull_request trigger, it can run automatically on PR updates. GitHub suppresses pull_request workflow runs when a pull request has merge conflicts with the target branch (gha#859). A direct /review path is available too, but only if the caller also enables issue_comment. claude.yml dispatches it via workflow_dispatch; keep the stub named claude-code-review.yml (or set claude.yml’s review-workflow-file to match) so the dispatch resolves.
If you have disabled claude.yml, enable issue_comment in the stub and set the repository variable CLAUDE_AGENT_DISABLED to true, so its dispatch-on-comment job also accepts an @claude review mention. Both are required: that job only ever runs on an issue_comment event, so the variable on its own changes nothing. Without them a repo with the agent off has no mention path, and the phrasing people reach for silently does nothing. Leave the variable unset while the agent is live, or the mention is answered twice.
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
runs-on |
string | empty (CI_RUNNER variable, else ubuntu-latest) |
Runner for every job, except the five gate jobs when gate-runs-on is set. Set a self-hosted label only for a private repository. |
gate-runs-on |
string | '' |
Runner for the short gate jobs (preempt-previous, gather-context, find-fallback-verdict, require-review, require-clean-verdict). They only call the GitHub API or report a result, so a caller on a busy self-hosted pool can send them to a hosted runner such as ubuntu-latest. Empty means the same runner as runs-on. |
pr-number |
string | '' |
PR number to review (set by claude.yml’s workflow_dispatch; empty on pull_request runs). |
prompt-addendum |
string | '' |
Repo-specific review guidance appended to the plugin prompt. |
checkout-submodules |
boolean | false |
Check out submodules so the reviewer can read their contents. Private submodules require the SUBMODULES_TOKEN secret. |
submodule-paths |
string | '' |
Whitespace-separated submodule paths to check out when checkout-submodules is true (for example latex-macros). Empty checks out every submodule. Use it when the repo has a private submodule the review job cannot clone and the reviewer only needs a public one. |
allowed-bots |
string | 'github-actions[bot]' |
Comma-separated bot actors the reviewer accepts, so a dispatched review is not rejected as non-human. |
track-progress |
boolean | false |
Ignored (gha#580). Tag mode posts a live tracking comment through the github_comment MCP server (mcp__github_comment__update_claude_comment) and hardcodes git write tools; it does not start github_inline_comment unless mcp__github_inline_comment__* or mcp__github__* is allowlisted. All of that needs a writable forge token in the model job. This workflow splits those: the model job’s GITHUB_TOKEN has no write scopes, and the posting job submits the summary after the model finishes. The input is kept so existing callers do not fail at the call gate. |
show-full-output |
boolean | false |
Show the full JSON output from Claude Code. Turn it on to diagnose a review that fails inside the model call. |
apt-packages |
string | '' |
Space-separated system packages to apt-get install before the review runs (e.g. maxima for a computer algebra system). Empty to skip. |
pip-packages |
string | '' |
Space-separated pip packages to install with pip3 --break-system-packages before the review runs (e.g. sympy). Empty to skip. |
lab-manual |
boolean | true |
Hold the PR to the SERG lab manual (coding style/practices, repository conventions) during review, in addition to correctness. Default true; set false for repos where the R/tidyverse/Quarto-focused manual doesn’t apply (e.g. a GitHub Actions, shell, or docs repo), so the reviewer isn’t handed guidance irrelevant to the diff. |
check-latex-macros |
boolean | true |
Review the math a PR adds or changes against the lab’s semantic macros (Morrison-Lab/macros): a symbol that has a macro must use it, with no literal letters, raw operators, raw exponentials, hat forms or deprecated aliases. On by default; the reviewer skips the check on a diff with no math, but the guidance is still in the prompt. The checks read definitions only from a locally checked-out submodule (the repo’s pinned version is authoritative), so set checkout-submodules: true too. Set false to switch it off. |
report-cost |
boolean | true |
Post a follow-up PR comment with the review’s dollar cost (total_cost_usd, summed across the initial attempt and any stub-review retry). See README.md’s feature-parity table for the upstream source citation. Set false to suppress it. |
model |
string | '' |
Model for the reviewer to run on, passed through as the Claude Code CLI’s --model flag (e.g. claude-opus-4-8). Empty (default) falls through to claude-code-action’s own default. Unlike claude.yml, this workflow has no generic claude-args passthrough, so this is the only way to influence the model. |
use-ai-config |
boolean | true |
Install the Morrison-Lab/ai-config plugin (ai-config@Morrison-Lab) alongside the built-in code-review@claude-code-plugins, so the reviewer applies the lab’s shared review conventions rather than only the calling repo’s own CLAUDE.md. Set false to opt out. |
use-posit-skills |
boolean | true |
Install the posit-dev/skills plugin (posit-dev@posit-dev-skills) alongside the built-in code-review@claude-code-plugins and ai-config@Morrison-Lab, so the reviewer applies Posit’s critical code review guidelines (including critical-code-reviewer). Set false to opt out. |
reference-repos |
string | 'Morrison-Lab/gha' |
Space-, comma-, or newline-separated list of GitHub repositories (owner/repo or owner/repo@ref) checked out into .reference-repos/ so the reviewer can inspect their files and schemas directly. Set to 'none' or '' to disable. |
reference-repos-token |
string | '' |
Optional GitHub token with read access to private reference repositories (e.g. SUBMODULES_TOKEN or a PAT). Public reference repositories clone anonymously. |
plugin-marketplaces |
string | '' |
Extra newline-separated plugin marketplace Git URLs, added on top of the marketplaces this workflow already uses (anthropics/claude-code.git for the code-review plugin, Morrison-Lab/ai-config.git unless use-ai-config is false, and posit-dev/skills.git unless use-posit-skills is false). Pair with plugins below; empty adds none. |
plugins |
string | '' |
Extra newline-separated plugin refs to install (<plugin>@<marketplace-name>), added on top of the built-in code-review@claude-code-plugins, ai-config@Morrison-Lab, and posit-dev@posit-dev-skills. Each marketplace name must match the name that marketplace declares in its own .claude-plugin/marketplace.json, not the URL given in plugin-marketplaces. |
extra-secret-names |
string | '' |
Space-, comma-, or newline-separated caller secret names to export into the reviewer environment (e.g. 'EPI202_TOKEN EPI204_TOKEN'). Requires secrets: inherit on the caller uses:. Secret names must match [A-Z_][A-Z0-9_]* and cannot shadow protected workflow environment variables. Empty to skip. |
timeout-minutes |
number | 60 |
Job timeout in minutes for the agent job. Default 60 is the hang ceiling, not a budget; lower it to match a consumer’s own cap, which a caller cannot otherwise do because GitHub rejects timeout-minutes on a job that uses uses:. |
Secrets
| Secret | Required | Description |
|---|---|---|
CLAUDE_CODE_OAUTH_TOKEN |
no | OAuth token for Claude Code (Claude Max plan). |
ANTHROPIC_API_KEY |
no | Direct Anthropic API key (or GitHub App via /install-github-app), used instead of CLAUDE_CODE_OAUTH_TOKEN. |
SUBMODULES_TOKEN |
no | Read access to private submodules. |
Variables
| Variable | Required | Description |
|---|---|---|
CLAUDE_CODE_ACCOUNT_EMAIL |
no | Email address or identifier of the Claude Code account associated with the OAuth token (set at repo or org level). Displayed in quota-skip notices. Falls back to CLAUDE_ACCOUNT_EMAIL or CLAUDE_ACCOUNT. |
CLAUDE_AGENT_DISABLED |
no | Set true on repos where claude.yml is disabled, allowing claude-code-review.yml’s dispatch-on-comment job to answer @claude review mentions directly. |
Permissions
Grant contents: read, pull-requests: write, issues: write, actions: read, checks: read (grant it; optional under @v2, which no longer requests it per gha#832, but required by the model job as of @v3), and add either the CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY secret. The model job’s GITHUB_TOKEN has no write scopes (contents / pull-requests / issues / actions / checks: read); write is confined to jobs that never run the model (gather-context stashes reviewers; post-review downloads the review artifact and comments) (gha#580). actions: read is required on the caller: a permissions: block sets unspecified scopes to none, and post-review needs it to download the packed artifact (the model job also uses it for the github_ci MCP server). Grant checks: read regardless of which tag you pin, but note what it does and does not buy. actions: read covers workflow runs but not GET .../commits/{ref}/check-runs, so without checks: read the reviewer’s check-status reads fail with HTTP 403 and a clean diff can be reported as blocked (ucdavis/bcs#964). The model job briefly lost this scope on @v2: a called workflow cannot request a permission its caller lacks, so the run ends in startup_failure before any job begins, and @v2’s original grant of checks: read on the model job broke startup for 17 of the 18 repositories pinning this workflow at @v2 that had not yet added the caller-side grant above (gha#831). Dropping the scope from the model job un-broke those callers but reopened the 403 for everyone – which is what v3 fixes by reinstating it there, now that the caller-side grant above is established practice. Keep granting checks: read regardless: it is optional under the currently-tagged @v2 (which no longer requests it, per gha#832) but required by the model job once you adopt @v3. A Dependabot bump from @v2 to @v3 is where a missing grant shows up, and it shows up as an absence: the bumped run ends in startup_failure, so the PR carries no review check at all rather than a red one. Add checks: read to the caller job in that same PR before merging it (gha#1006; measured on Morrison-Lab/mds#78).
The require-review gate
Put review / require-review in branch protection’s required checks, not review / claude-review: the review job reports success in cases where no verdict was produced, and this gate job exists to distinguish them.
Read its result as follows.
| Result | What it attests |
|---|---|
| Green | A reviewer ran and the job did not fail. Not that the reviewer approved — read the posted verdict for that. |
| Gray (skipped) | No review ran. Does not block merging. |
| Red | The review job did not succeed. Blocks merging. |
The gate skips gray whenever no review was attempted. That covers anything that made the claude-review job’s own if: false — a draft, fork, or bot-authored PR, a dispatch blocked by the fork/Dependabot guard, or the context-gathering job having been cancelled (or, on a dispatched review, failed — an ordinary PR event keeps its event-pinned head, so a failed context-gather does not suppress its review) — plus a claude-review run canceled by a newer run of the same PR (cancel-in-progress, #585): the superseded run’s gate skips gray rather than going red, and the surviving run’s review is the one to read. The gate also skips gray when the posting job reported the review stale — the PR head moved between the review and the post, so no comment was posted and the next head’s own run carries the verdict. Two further cases where the job ran and stopped early also skip gray:
- the API quota was exhausted — either before any review work happened, or part-way through the run, which leaves a
429behind after real turns and real cost (#520); - restoring default-branch copies of
.github/workflows/failed, so the review was skipped rather than executing workflow YAML from the PR head (#598).
In those two cases require-clean-verdict fails instead of skipping, since no verdict exists for the head (#1019).
PRs that edit workflow files are reviewed when that restore succeeds. The job replaces .github/workflows/ with the default-branch tree after checkout and passes github_token so claude-code-action skips its OIDC workflow-content check. On a dispatched review it also omits --ref, so GitHub executes the default-branch caller rather than the PR head’s YAML. A files-list response shorter than the PR’s changed_files count, reaching the 3000-file endpoint cap (gha#917), or targeting a base branch other than the default branch (gha#916), is treated the same as a failed listing, so a large or stacked PR cannot hide a workflow edit behind the endpoint cap or base diff. Check-runs for that no---ref dispatch land on the default branch (#285); the review comment still posts on the PR.
A gray skip here still means no review ran. Do not read it as an all-clear. A later re-run can recover if fetching the default branch was the problem.
The require-clean-verdict gate
Where require-review verifies review delivery (the review ran and was posted), require-clean-verdict acts as an opt-in server-side verdict gate (gha#767). Put review / require-clean-verdict in branch protection’s required checks to enforce that only PRs with an affirmatively clean AI review verdict (“Ready for merge”, “Clean”, “Approved”) can merge.
| Result | What it attests |
|---|---|
| Green | The review completed, was posted for the current PR head, and produced an affirmatively clean verdict (“Ready for merge”). |
| Gray (skipped) | No review was due on this run (draft, fork, bot author, cancellation, or stale head). Does not block merging. |
| Red | The review failed, produced a non-clean verdict (“Needs more work”, “Changes requested”, “Blocked”, or unaddressed findings), or never ran on this head because the API quota was exhausted or the default-branch workflow restore failed (gha#1019). A quota skip is green instead when a clean fallback review of the current head is recorded on the PR: a separate-agent review comment from an owner, member or collaborator that carries the agent-authorship marker and names the head sha in a Reviewed-Commit: line (gha#1163). Blocks merging when configured as a required check. |
Fallback review on a quota skip
When the API quota is exhausted, require-clean-verdict goes green only if a clean separate-agent review of the current head is recorded as a PR comment. A comment counts when all of these hold:
- Its author is an owner, member or collaborator. The PR author’s own comment counts, because the lab’s agent sessions post under the account holder’s login.
- It carries the agent-authorship marker.
- It names the full head sha on a
Reviewed-Commit:line. - The marker and that line sit in the comment’s own prose. Text inside a code fence, a blockquote or an indented code block is ignored, so quoting a review does not record one.
- The classifier reads its verdict as clean.
The newest such comment decides, and a push retires it (gha#1182).
Concurrency
Do not declare a top-level concurrency: block in your caller workflow. claude-code-review.yml manages per-PR concurrency internally on its claude-review job (group: claude-review-<PR>). Adding a top-level concurrency: block in the caller with a PR-scoped group name causes GitHub Actions to detect a deadlock between the top-level workflow and the nested job, cancelling the run immediately (gha#437).
Example
# Copy to .github/workflows/claude-code-review.yml in your repo.
# NOTE: requires either CLAUDE_CODE_OAUTH_TOKEN (Claude Max plan) or
# ANTHROPIC_API_KEY (direct API / GitHub App via /install-github-app). Pass
# secrets explicitly (below) rather than via `secrets: inherit`: GitHub only
# inherits secrets into a reusable workflow owned by the same org/user, so a
# cross-owner caller (e.g. a UCD-SERG-org repo calling this d-morrison
# user-owned workflow) inherits an empty token and the run fails env-validation.
# Default: mention-triggered only. claude.yml re-dispatches this workflow on an
# `@claude review` mention (and after an @claude run pushes commits) via
# workflow_dispatch. Keep the file named claude-code-review.yml (or set
# claude.yml's review-workflow-file to match) so the dispatch resolves.
# Optional: uncomment pull_request for automatic review, and uncomment
# issue_comment to allow trusted collaborators to trigger `/review` directly.
# If you have DISABLED claude.yml, do BOTH of these, or nothing happens: uncomment
# the issue_comment trigger below, and set the repository variable
# CLAUDE_AGENT_DISABLED to `true`. dispatch-on-comment only ever runs on an
# issue_comment event, so the variable on its own changes nothing.
name: Claude Code Review
on:
# Optional: enable automatic review on PR activity.
# pull_request:
# types: [opened, synchronize, ready_for_review, reopened]
# Optional: direct on-demand `/review` path (independent of @claude), plus
# the opt-in `@claude review` fallback for repos with the agent disabled.
# issue_comment:
# types: [created]
workflow_dispatch:
inputs:
pr_number:
description: "Pull request number to review"
required: true
type: string
jobs:
review:
permissions:
contents: read
pull-requests: write
issues: write
actions: read
checks: read # keep: required by @v2 until the slide, and again at the v3 (gha#833)
uses: Morrison-Lab/gha/.github/workflows/claude-code-review.yml@v2
secrets:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # Max-plan OAuth; empty when using API key
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} # direct API key; empty when using OAuth
SUBMODULES_TOKEN: ${{ secrets.SUBMODULES_TOKEN }} # optional; empty when unset
with:
# Wires the workflow_dispatch input through so claude.yml can re-dispatch
# a review on Claude's commits; empty (and ignored) on pull_request runs.
pr-number: ${{ inputs.pr_number }}
# checkout-submodules: true # SUBMODULES_TOKEN secret only for private submodules
# submodule-paths: 'latex-macros' # only these submodules (default: all); needs checkout-submodules: true
# allowed-bots: 'github-actions[bot],claude' # accept more bot actors (default: github-actions[bot])
# track-progress: true # ignored (gha#580); tag mode needs a writable token in the model job
# lab-manual: false # skip the R/tidyverse SERG lab manual (default true; set false for non-R/Quarto repos)
# reference-repos: 'Morrison-Lab/gha' # check out reference repos into .reference-repos/ for the reviewer
# apt-packages: maxima # system packages for the reviewer's Bash tool, e.g. a CAS
# pip-packages: sympy # pip packages, e.g. a computer-algebra system for math-heavy repos
# check-latex-macros: false # on by default: review math against Morrison-Lab/macros; needs checkout-submodules: true
# report-cost: false # suppress the dollar-cost comment (default true)
# model: claude-opus-4-8 # override the reviewer's model (default: claude-code-action's own default)
# use-ai-config: false # skip the Morrison-Lab/ai-config plugin (installed by default)
# use-posit-skills: false # skip the posit-dev/skills plugin (installed by default)
# plugin-marketplaces: https://github.com/<owner>/<repo>.git # further plugin sources
# plugins: <plugin>@<marketplace-name>
# extra-secret-names: EPI202_TOKEN EPI204_TOKEN # export caller secrets to reviewer env (needs secrets: inherit)
# timeout-minutes: 50
# prompt-addendum: |
# Repo-specific review guidance (e.g. Quarto/R conventions to enforce).See examples/claude-code-review.yml for the full caller stub.