cool-solution — dev.blog
Technologies

Claude Code in GitHub Actions: automated PR review with claude-code-action

When I work with Claude Code in GitHub Actions, my favorite moment is opening a pull request and finding a review already written shortly afterward: risks identified, lines pinpointed and severity assigned. The runner installs Claude Code and uses the same agent loop available locally through the action's Agent SDK integration. In this tutorial I build the flow from scratch: I start with `claude -p` in the terminal, install the GitHub App with `/install-github-app`, write two separate workflows (automatic review and interactive `@claude`) and finish with guardrails for tools, turns, budget and permissions.

Illustration: a GitHub pull request flows through an Actions workflow where Claude Code runs a non-interactive review and posts a comment with the findings.

🔍 Non-interactive mode: claude -p

Everything Claude Code does in an interactive session — reading files, running commands and reasoning about code — it can also do without an interface. The `-p` flag (or `--print`) executes one prompt, prints the result and exits. This is non-interactive mode, formerly called headless mode, and it is the foundation for scripts and automation. Context comes from the working directory and output goes to stdout like any Unix tool.

CI needs several distinct guardrails. `--output-format` selects `text`, `json` or `stream-json`; JSON includes the result, session ID and an estimated cost. `--allowedTools` pre-approves tools, but it is not a deny-list by itself. For a restricted run I also use `--permission-mode dontAsk` and explicitly deny write tools. `--max-turns` limits agent round trips, while `--max-budget-usd` is the actual estimated-cost ceiling.

Terminal · non-interactive mode, three variants
# 1. Basic: prompt in, result on stdout, exit
claude -p "List the files touched most often in the last 20 commits"

# 2. JSON output: result + cost + session ID
claude -p "Summarize the project architecture" \
  --output-format json

# 3. With guardrails: permissions, turns and budget
claude -p "Find TODO and FIXME comments and group them by area" \
  --allowedTools "Read,Grep,Glob" \
  --disallowedTools "Edit,Write" \
  --permission-mode dontAsk \
  --max-turns 5 \
  --max-budget-usd 0.50

Headless mode · official docs

The -p flag turns Claude Code into a composable Unix tool: prompt in, text or JSON out, exit code at the end of the run.

🧪 Reviewing locally, before CI

Before moving a review prompt into a workflow, I test it in the terminal. The loop is short: run the script, read the review, improve the prompt and run it again. JSON output includes the `total_cost_usd` estimate, which helps me choose `--max-turns` and `--max-budget-usd`; I still use the Claude Console Usage page for authoritative spend.

The script fetches the base branch, calculates a deterministic diff and pipes it to Claude on stdin, so the agent does not need to run `git diff`. `dontAsk` denies tools that were not pre-approved and `--disallowedTools` blocks writes. The review produces only a report. In CI I apply the same principle by separating the commenting workflow from the one that may change code.

scripts/review-local.sh · non-interactive diff review
#!/usr/bin/env bash
# Headless review of the diff against the base branch.
set -euo pipefail

base="${1:-main}"

git fetch origin "${base}"
git diff "origin/${base}...HEAD" | claude --bare -p "Review the diff
received on stdin.
For each finding give: file, line, severity (high/medium/low),
problem and proposed fix. Focus on bugs, security,
performance. Ignore style." \
  --allowedTools "Read,Grep,Glob" \
  --disallowedTools "Edit,Write" \
  --permission-mode dontAsk \
  --max-turns 10 \
  --max-budget-usd 1.00 \
  --output-format json
Diff on stdin, writes denied, a 10-turn limit and a USD 1 estimated budget. --bare requires ANTHROPIC_API_KEY and makes the run reproducible.

⚙️ Setup: /install-github-app and secrets

The bridge between the repository and the Anthropic API starts with one command: I open `claude` in the repository and run /install-github-app. The wizard guides me through installing the official GitHub App, registering the `ANTHROPIC_API_KEY` secret and creating a starter workflow; repository-admin access is required. I can also install the app from `github.com/apps/claude`, add the secret in the Actions settings and create the YAML manually.

The one place worth slowing down is the workflow permissions block. The review template ships conservative, with `pull-requests: read`: Claude can read the PR but cannot post the review comment. To get findings directly on the pull request you need to raise it to write — and that's a choice I make deliberately, workflow by workflow, not a repo-wide default.

The full flow: from opened PR to review comment
Flow diagram: the opened pull request triggers the GitHub Actions workflow, the runner executes Claude Code non-interactively with claude_args, and the review lands back on the PR as a comment.

The runner executes the same agent loop I use locally: repository checkout, explicit guardrails and a final comment through the GitHub API.

🤖 The automatic review workflow

The first workflow runs automatically when a non-draft pull request is opened, updated, reopened or marked ready for review. With an explicit `prompt`, anthropics/claude-code-action@v1 enters automation mode. `track_progress: true` guarantees a progress comment that also carries the final summary, while the `gh pr` and inline-comment MCP tools let Claude publish findings on the PR.

`contents: read` keeps the code read-only, `pull-requests: write` allows comments and `id-token: write` is required by the official GitHub App. A shallow checkout is sufficient because the action receives PR context and can use `gh pr diff`; full history is unnecessary. The job deliberately excludes forks because fork-triggered `pull_request` workflows do not receive `ANTHROPIC_API_KEY` and their `GITHUB_TOKEN` remains read-only.

.github/workflows/claude-pr-review.yml · automatic review
name: Claude PR Review

on:
  pull_request:
    types: [opened, synchronize, reopened, ready_for_review]

concurrency:
  group: claude-pr-review-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  review:
    if: >
      github.event.pull_request.draft == false &&
      github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
      id-token: write
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 1

      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          track_progress: true
          prompt: |
            REPO: ${{ github.repository }}
            PR NUMBER: ${{ github.event.pull_request.number }}

            Review this pull request.
            For each finding: file, line, severity (high/medium/low),
            problem, proposed fix. Focus on bugs,
            security and performance. Ignore style.
            Use gh pr comment for the summary and the MCP tool
            for inline comments, with confirmed: true.
          claude_args: |
            --max-turns 15
            --max-budget-usd 1.00
            --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
            --disallowedTools "Edit,Write"

anthropics/claude-code-action · GitHub

Automation mode with tracking, explicit comment tools, read-only code and cancellation of superseded runs.

💬 @claude in comments: interactive mode

The second workflow requires an explicit mention. I write `@claude how would you handle pagination here?` in a comment and the agent answers; with `@claude fix the validation on this endpoint`, it may implement the change and create a commit on the PR branch. Without a `prompt`, the action enters interactive mode and applies its access-control check: by default, only a user with repository write access can trigger it.

Here `contents: write` is necessary because the agent may create commits. I therefore keep the workflows separate: automatic review has read-only code access, while the interactive workflow has broader powers but starts only after an authorized mention. I also include `pull_request_review` so mentions in submitted review bodies work alongside general and inline comments.

.github/workflows/claude.yml · answers mentions
name: Claude

on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
  pull_request_review:
    types: [submitted]

jobs:
  claude:
    if: |
      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude'))
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
      issues: write
      id-token: write
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 1

      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          claude_args: --max-turns 25 --max-budget-usd 2.00
No prompt → interactive mode: the action reads the mention, replies in the thread or commits to the PR branch.

🎛️ claude_args: model, permissions and turns

`claude_args` passes CLI flags to the action. I use `--model` to select a model, `--max-turns` to limit agent round trips and `--max-budget-usd` to stop when the client-side estimate reaches the threshold. `--allowedTools` pre-approves specific capabilities; the effective boundary comes from combining it with `--disallowedTools`, the permission mode and GitHub's `permissions` block.

One easy-to-miss detail is the permission syntax. `Bash(git diff:)` authorizes commands beginning with `git diff`; the equivalent modern form is `Bash(git diff *)`. Without the boundary before the wildcard, `Bash(git diff)` also matches `git diff-index`. I therefore keep patterns specific and add only tools required for the expected result.

The result on the PR: findings sorted by severity
Mock of a GitHub pull request with the review comment generated by Claude: three findings sorted by severity with file, line and proposed fix.

Every run produces a structured comment: severity, file:line, problem and proposal. The format is decided by the prompt, not the action.

⚠️ Mistakes to avoid

The first mistake is skipping local tuning. Iterating on a YAML prompt requires repeated pushes and runner waits; a prompt tested with `claude -p` reaches CI in much better shape. The second is treating the agent's review as a verdict: it is a first pass that clears mechanical problems — missing null checks, edge cases and suspicious queries — and frees human reviewers to focus on architecture and design choices. A human still approves the PR.

The third mistake is economic: `--max-turns` limits agent duration, while `--max-budget-usd` sets the estimated-cost threshold. I start low, observe `total_cost_usd` and verify authoritative spend in the Claude Console. The final mistake is about security: secrets are not passed to fork-triggered `pull_request` workflows. I do not bypass that protection with `pull_request_target` while checking out untrusted fork code into the main workspace.

  • Tune the prompt locally with claude -p: iterating in CI costs minutes per attempt; in the terminal it takes seconds.
  • Two workflows, two perimeters: automatic review is read-only; write access lives only in the @claude workflow invoked by a human.
  • Limit turns and budget: --max-turns controls round trips; --max-budget-usd controls estimated spend per run.
  • Minimal permissions that work: pull-requests: write is needed for the comment; contents: write only where the agent actually commits.
  • AI review doesn't approve: it clears the mechanical problems, but merging stays a human decision.

🧪 Verify with a test PR

Before enabling the workflow across the repository, I open a same-repository test PR with a harmless deliberate defect. I verify that the run starts, the comment is published, inline findings point to the correct files and lines, and a superseded run is canceled after a new push. Only then do I merge the workflow into `main`.

Create a test PR in the same repository
git switch -c test/claude-pr-review
# Introduce a harmless defect and commit it
git add .
git commit -m "test: exercise Claude PR review"
git push -u origin test/claude-pr-review
gh pr create --fill --base main
gh pr checks --watch
This workflow policy limits the test to same-repository PRs; fork support requires a separate authentication design.

✅ Final checklist

Before declaring the automatic review "live" on a repo, I always walk through the same seven points. They are the difference between a workflow that produces useful reviews and one that generates expensive noise — and I learned each of these by getting it wrong at least once.

If any item isn't green, the workflow doesn't go to `main`. That holds for side projects and holds twice for work repos, where a runaway run across twenty open PRs is a bill, not an experiment.

Pre-activation checklist
[ ] Review prompt tuned locally with claude -p
[ ] ANTHROPIC_API_KEY registered as a repo secret
[ ] Review workflow: pull-requests: write, contents: read
[ ] Separate @claude workflow for code changes
[ ] Required gh/MCP tools can publish comments
[ ] --max-turns and --max-budget-usd calibrated
[ ] Same-repository/fork policy explicitly defined
Seven checks, two minutes. It's the minimum before switching on automatic review on a shared repository.

Frequently asked questions about Claude Code GitHub Actions

What is Claude Code headless mode?

It's the non-interactive execution of the CLI: with the -p (--print) flag Claude Code runs the same agent loop as a normal session — reading files, running commands, reasoning about code — but prints the result and exits, with no UI. It's the foundation of every automation: local scripts, cron jobs, CI/CD. The output can be plain text, structured JSON or a line-by-line stream of JSON events.

What does claude-code-action do compared to a regular review bot?

Anthropic's official action runs a Claude Agent SDK-powered agent inside the GitHub Actions runner: it can use a real shell, read checked-out files and — in workflows where you allow it — modify code and commit. It supports Claude Code CLI arguments while adding GitHub event context, authentication and comment tools.

How much does an automatic PR review cost?

It depends on the model, diff size and consumed turns. --max-turns limits round trips, --max-budget-usd stops the run at an estimated threshold and --model can select a cheaper model. The JSON total_cost_usd field is a client-side estimate; I use the Claude Console Usage page for authoritative spend.

Why separate the review workflow from the @claude one?

Least privilege. The automatic review runs without a human in the loop on eligible same-repository PRs: I give it contents: read plus only the tools needed to inspect and comment, so it cannot modify repository code. The @claude workflow can write code and commit (contents: write), but only starts when an authorized person explicitly mentions it. Broad automation with small powers, big powers only on human request.

Does Claude's review replace the human one?

No, it precedes it. The agent is good at mechanical problems — missing null checks, unhandled edge cases, N+1 queries and inconsistencies with the rest of the codebase — and flags them before a colleague begins the manual review. Human review remains necessary for architecture, product choices and context that doesn't live in the repo. A person always approves the merge.

Can I use the action without a direct Anthropic API key?

Yes. Anthropic Workload Identity Federation replaces the static key with federation identifiers and id-token: write. Bedrock, Vertex AI and Microsoft Foundry require provider-specific use_bedrock/use_vertex/use_foundry inputs, OIDC setup, environment values and model identifiers. The review prompt remains reusable, but the authentication workflow changes.

Let's talk

If this topic is relevant to you, write to me: comparing notes on code and AI is always time well spent.

All articles

More articles from the blog

semantic-caching-llm-dotnet-redis.mdJuly 27, 2026tips-claude-claude-md-luglio-2026.mdJuly 24, 2026claude-code-pretooluse-hook-proteggere-env-segreti-2026.mdJuly 24, 2026