Claude Code in GitHub Actions
How do you review a pull request with Claude Code in GitHub Actions?
The complete workflow that reviews every pull request with Claude Code: the pull_request trigger, a read-only checkout, the one permission the comment needs, what the review says, and what a review costs.

Listen to this guide
The short answer
With one workflow file on the pull_request trigger. GitHub starts a runner every time a pull request opens or gets a push, Claude Code reads that diff in the checkout, and the run leaves what it found as a comment on the pull request.
What differs from an agent that writes code is the permissions block. A reviewer takes contents: read, so it can read the diff and never push over it, and pull-requests: write, which is the one grant the comment needs.
What the run needs
- A repository with pull requests, and Actions on, which it is by default. The file goes on the default branch: GitHub reads the workflow for a pull request from there, not from the branch under review.
- An Anthropic API key from the Console, saved as the repository secret
ANTHROPIC_API_KEY. A Claude subscription signs in withclaude_code_oauth_tokenandCLAUDE_CODE_OAUTH_TOKENinstead. - The base branch on the runner.
actions/checkoutfetches one commit by default, and a diff needs both sides, so the step below asks for the full history. - Nothing on your machine. The runner installs Claude Code on every run, the same as the agent that writes the code does.
- A pull request from a fork is the exception.
pull_requesthands a fork's run a read-only token and no secrets at all, so the review step finds no key and the job fails on it. The section below says what to do instead.
The workflow file
- Save this as
.github/workflows/review.ymlon the default branch, and addANTHROPIC_API_KEYas a repository secret first, or the first run has nothing to sign in with.
name: Review
on:
pull_request:
types: [opened, synchronize, reopened]
# Read the code, write the comment: a reviewer never needs to push anything.
permissions:
contents: read
pull-requests: write
# A push replaces the review already running on the old head, so a busy morning bills one review, not five.
concurrency:
group: review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
review:
# A draft is still being written; reviewing every push to one costs money and says nothing useful.
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
# The base branch has to be on the runner too, or there is nothing to diff the head against.
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Review the diff
uses: anthropics/claude-code-action@v1
with:
# A Claude subscription signs in with claude_code_oauth_token and secrets.CLAUDE_CODE_OAUTH_TOKEN instead of this line.
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
# The run's cost lands in the job summary, where nobody has to open a log to find it.
display_report: true
prompt: |
Review pull request #${{ github.event.pull_request.number }} in ${{ github.repository }}.
The change is `git diff origin/${{ github.base_ref }}...HEAD` in this checkout, and only that.
Judge correctness first, then the tests, then whatever the repository's own guide asks for.
Read files around the diff before calling anything wrong; a hunk alone is not the whole story.
Post one comment with `gh pr comment ${{ github.event.pull_request.number }} --body-file`.
Name the file and the line of each finding, and say plainly when you found nothing.
claude_args: |
--allowedTools "Bash,Read,Glob,Grep"
--max-turns 30
--max-budget-usd 2
What the token may do
contents: readandpull-requests: writeare the whole grant. The first lets the checkout fetch the code, the second lets the run comment. Drop the second and the comment fails with a 403 after the review has already been paid for.- The block overrides the repository default. A repository still set to the permissive default would hand a reviewer write access to everything; naming both scopes in the file settles it whatever the setting is. Permissions and secrets covers the rest of the scopes.
- Never give a reviewer
contents: write. A run that can push can rewrite the branch it is judging, and no comment tells you it did. Branches and pull requests covers what a run that is meant to push looks like. - A fork's pull request is read-only on purpose.
pull_request_targetwould give it your secrets, and checking out the fork's head under it runs a stranger's code with them, which is the one mistake this trigger exists to prevent. - Review forks by asking for the review instead, on
issue_commentwith the same permissions, so a maintainer who has read the diff types@claude reviewand the run happens on your side. github_tokenis the job's own token. GitHub mints it per run, bounded by the block above, and it expires when the job ends, so the comment arrives fromgithub-actions[bot]rather than from a person.
What the review comment says
- One comment a run, naming the file and the line of each finding, ordered correctness first, then tests, then the repository's own conventions.
- It reads
CLAUDE.mdif the repository has one, so house rules, naming, and the layering a team argues about become review rules without being restated in the prompt. - It says when it found nothing, which matters more than it sounds: a silent reviewer is indistinguishable from a broken one.
- A comment is not an approving review. A branch protection rule asking for an approval still waits for a person, and that is the point: this reads everything and decides nothing.
- The prompt is the review policy. Ask for security only, or for the tests alone, and that is what the comment holds; the file is the whole configuration.
- A run that says something wrong is a prompt bug, not a reason to turn it off. The line about reading around the diff is there because a hunk on its own invites confident nonsense.
What it costs and how to cap it
--max-budget-usd 2stops Claude Code once the run's API spend reaches two dollars. It is the only cap denominated in the thing Anthropic bills you.--max-turns 30caps the model calls a review may make. A reviewer that has started reading the whole repository hits this before it reaches the budget.timeout-minutes: 15is GitHub's hard stop, and it caps the Actions minutes the job can bill. Costs, limits and troubleshooting covers what each cap does and does not stop.cancel-in-progress: trueis the cost control that matters most here. Ten pushes to one pull request would otherwise be ten reviews of nine stale diffs.- Narrow what gets reviewed with
paths-ignoreon the trigger, so a documentation typo or a lockfile bump starts nothing. - Outside the file, put a spend limit on the key's Console workspace and a budget on Actions. Those catch the case a cap in the file cannot: a cap you edited wrongly.
What Repo Fleet does about it
Repo Fleet is where you build workflows like this one and watch them run, across every repository you connect. The file stays what this guide built: GitHub Actions in your repository, on your runners, billed to your own Anthropic key. Repo Fleet holds no key and meters nothing.
- The trigger, its types, the two permission scopes, the concurrency group and the prompt are plain fields in a visual editor, with the YAML one tab away.
- Every edit is checked as you type by the checks that pass every block on this page, including the ones that catch a missing timeout and a grant wider than the run needs.
- The runs page lists each repository's runs newest first, so a review that started failing is visible without opening the Actions tab.
- Reviewing is one trigger away from the rest: the same file on
issuesimplements the issue, and onscheduleit works a queue overnight.
Install it on GitHub, choose the repositories it may see, and open this file in the editor. The next pull request gets read.