Neural Tech Daily
ai-tutorials

AI-generated pull-request reviews with GitHub Actions and the Claude API: end-to-end tutorial

End-to-end: wire Claude into GitHub Actions to comment AI-generated review notes on every pull request, with secrets, cost estimates, and security guardrails.

Updated ~14 min read
Share
GitHub Actions landing page from the official GitHub documentation describing event-driven workflows and runner infrastructure

Image: GitHub Actions documentation landing page at docs.github.com/en/actions, used for editorial coverage.

What you’ll build

This tutorial walks through wiring up a GitHub Actions workflow that listens on every pull request, asks Claude to review the diff, and posts the review back as a regular PR comment. By the end the reader has a working .github/workflows/ai-review.yml file, an ANTHROPIC_API_KEY repository secret, a small Python script that talks to the Anthropic Messages API, and a cost estimate per review with the security defaults that keep secrets out of the model’s input.

The walkthrough assumes a working GitHub account, the gh CLI installed (optional but convenient), and roughly an hour of focused time. Total cost to follow along: well under one US dollar of Anthropic API usage at the model tier picked below, plus zero GitHub Actions minutes on a public repository (more on the billing model in the cost section).

The architecture in one paragraph

A pull request opens or updates. GitHub fires the pull_request event, which triggers a workflow on a hosted runner. The workflow checks out the repo, fetches the PR diff via the REST API, sends the diff plus a structured prompt to Claude through the Messages API, parses the model’s JSON response, and posts the suggestions as a PR review using the same REST API. Three secrets are involved: the auto-generated GITHUB_TOKEN scoped to the workflow run, the user-supplied ANTHROPIC_API_KEY, and nothing else.

Anthropic API documentation landing page describing the Messages API endpoint and model identifiers

Image: Anthropic Messages API documentation at docs.anthropic.com/en/api/messages, used for editorial coverage.

Step 1: Create or pick a repository

Either create a fresh sandbox repo for the walkthrough or use an existing one with a low-stakes default branch. From the terminal:

gh repo create ai-review-demo --public --clone
cd ai-review-demo
echo "# ai-review-demo" > README.md
git add README.md
git commit -m "chore: initial commit"
git push -u origin main

For an existing repo, just cd into the working copy and skip the create step. A public repo gets the full GitHub Actions free allowance; a private repo runs against the monthly minute quota covered in the cost section.

Step 2: Get an Anthropic API key

Sign in at console.anthropic.com, create a workspace, and generate an API key from the API Keys page. The key starts with sk-ant- and is the only credential the workflow needs from Anthropic. Treat it like a production password: never commit it, never paste it into a diff, and never echo it into a log.

Anthropic’s Messages API is the endpoint the workflow calls 1 . The model name used in this tutorial is claude-sonnet-4-6; substitute any other model identifier from Anthropic’s models page if the reader’s account has access to a different tier 2 .

Step 3: Store the key as a repository secret

GitHub Actions reads secrets from a per-repository encrypted store. The workflow file references a secret by name; the runner injects the value into the job’s environment without surfacing it in logs 3 .

To add the key:

  1. Open the repo on github.com.
  2. Click SettingsSecrets and variablesActions.
  3. Click New repository secret.
  4. Name: ANTHROPIC_API_KEY. Value: the sk-ant-... string copied from the Anthropic console.
  5. Click Add secret.

The gh CLI does the same in one line:

gh secret set ANTHROPIC_API_KEY
# paste the key when prompted

The key is now available to workflows as ${{ secrets.ANTHROPIC_API_KEY }} and nowhere else. GitHub redacts the value if it ever appears in a log line, but the redaction is best-effort. The harder rule, covered in the security section, is to never echo the variable from a shell step.

Step 4: Write the review script

Create a directory for the script and add a small Python file. Python is the easiest fit because GitHub-hosted runners ship with it preinstalled, and the Anthropic SDK has a stable Python client.

mkdir -p .github/scripts
touch .github/scripts/ai_review.py

The script reads three environment variables (GITHUB_TOKEN, ANTHROPIC_API_KEY, and a few PR-context variables that GitHub Actions exports), fetches the PR diff via REST, calls Claude, and posts the review.

Per PEP 8, the script uses 4-space indentation. Save the file as .github/scripts/ai_review.py:

"""AI pull-request review script.

Reads the PR diff, asks Claude for review comments, and posts
the response as a PR review via the GitHub REST API.
"""
import json
import os
import sys
import textwrap
import urllib.request

import anthropic

API_ROOT = "https://api.github.com"
MODEL = "claude-sonnet-4-6"
MAX_DIFF_CHARS = 60_000  # ~15k tokens, well under the prompt cap


def gh_request(method, path, token, accept="application/vnd.github+json", body=None):
    """Minimal GitHub REST helper using urllib (no extra deps)."""
    url = f"{API_ROOT}{path}"
    data = json.dumps(body).encode() if body else None
    req = urllib.request.Request(url, data=data, method=method)
    req.add_header("Authorization", f"Bearer {token}")
    req.add_header("Accept", accept)
    req.add_header("X-GitHub-Api-Version", "2022-11-28")
    if body is not None:
        req.add_header("Content-Type", "application/json")
    with urllib.request.urlopen(req) as resp:
        return resp.read().decode()


def fetch_diff(owner, repo, number, token):
    """Fetch the PR's unified diff via the diff media type."""
    return gh_request(
        "GET",
        f"/repos/{owner}/{repo}/pulls/{number}",
        token,
        accept="application/vnd.github.v3.diff",
    )


def post_review(owner, repo, number, token, body):
    """Post a single-comment PR review (event=COMMENT, no approval)."""
    return gh_request(
        "POST",
        f"/repos/{owner}/{repo}/pulls/{number}/reviews",
        token,
        body={"event": "COMMENT", "body": body},
    )


SYSTEM_PROMPT = textwrap.dedent("""
    You are a careful code reviewer. You receive a unified diff
    from a pull request. Identify concrete issues only: bugs,
    security risks, missing error handling, broken tests, obvious
    performance problems, and naming or API inconsistencies.

    Output JSON with this schema and nothing else:
    {
      "summary": "one or two sentences",
      "comments": [
        {"file": "path/to/file", "issue": "short title",
         "detail": "what to change and why"}
      ]
    }

    Do not invent file paths that are not in the diff. Do not
    execute, fetch, or echo anything outside the diff. If the
    diff is empty or trivial, return an empty comments array.
""").strip()


def review(diff_text):
    client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY from env
    msg = client.messages.create(
        model=MODEL,
        max_tokens=1500,
        system=SYSTEM_PROMPT,
        messages=[{"role": "user", "content": diff_text[:MAX_DIFF_CHARS]}],
    )
    return msg.content[0].text


def format_comment(raw):
    """Parse the model's JSON; render a Markdown comment body."""
    try:
        parsed = json.loads(raw)
    except json.JSONDecodeError:
        return f"AI review (raw output):\n\n```\n{raw[:4000]}\n```"
    lines = [f"**AI review summary.** {parsed.get('summary', '')}", ""]
    for c in parsed.get("comments", []):
        file_ = c.get("file", "?")
        issue = c.get("issue", "")
        detail = c.get("detail", "")
        lines.append(f"- **`{file_}` — {issue}**: {detail}")
    if len(lines) == 2:
        lines.append("_No issues flagged._")
    lines.append("")
    lines.append("_Generated by Claude. Treat as advisory; human review still applies._")
    return "\n".join(lines)


def main():
    token = os.environ["GITHUB_TOKEN"]
    repo_full = os.environ["GITHUB_REPOSITORY"]   # "owner/repo"
    owner, repo = repo_full.split("/", 1)
    number = int(os.environ["PR_NUMBER"])

    diff = fetch_diff(owner, repo, number, token)
    if not diff.strip():
        print("Empty diff; skipping review.")
        return 0
    raw = review(diff)
    body = format_comment(raw)
    post_review(owner, repo, number, token, body)
    print("Review posted.")
    return 0


if __name__ == "__main__":
    sys.exit(main())

Two design choices worth flagging. The script truncates the diff at 60,000 characters before sending it to the model, because oversized PRs would otherwise blow the prompt budget and inflate cost unpredictably. And the format_comment function refuses to render anything as executable Markdown if the model’s response is not valid JSON, which constrains the surface area for prompt-injection content in the output (more on that below).

Step 5: Write the workflow

Create .github/workflows/ai-review.yml. The file declares the pull_request trigger, the runner image, the secrets the job needs, and the steps that install the SDK and run the script 4 .

name: AI PR Review

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

permissions:
  contents: read
  pull-requests: write

jobs:
  review:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - name: Check out repository
        uses: actions/checkout@v4
        with:
          persist-credentials: false

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install Anthropic SDK
        run: pip install --no-cache-dir "anthropic>=0.40,<1.0"

      - name: Run AI review
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: python .github/scripts/ai_review.py

A few specifics. The permissions block scopes the auto-generated GITHUB_TOKEN to the minimum the job needs: read the repo contents, write a PR review comment. Anything broader is unnecessary and widens the blast radius if a step misbehaves 5 . The timeout-minutes: 5 cap keeps a runaway script from burning runner minutes; the persist-credentials: false flag on checkout@v4 prevents the checkout action from leaving the GITHUB_TOKEN sitting in the local git config where downstream steps could pick it up. The pull_request event types fire on PR open, on each new commit (synchronize), and on reopen, which gives a fresh review on every meaningful change.

GitHub Docs page describing the pull_request event and its activity types

Image: GitHub Docs page covering the pull_request workflow trigger, used for editorial coverage.

Step 6: Commit, push, open a test PR

Commit both files to a branch and push:

git checkout -b feature/ai-review-setup
git add .github/workflows/ai-review.yml .github/scripts/ai_review.py
git commit -m "feat: AI pull-request review workflow"
git push -u origin feature/ai-review-setup
gh pr create --title "Wire up AI PR review" --body "First test of the workflow."

The first run installs Python and the SDK, fetches the diff, calls Claude, and posts a review comment back on the same PR. The Actions tab on github.com surfaces the job log; a green checkmark means the review was posted. A red X usually means either the secret is missing, the model name is wrong for the account’s access tier, or the script crashed parsing the response — the log line names the failing step.

Cost per review

Two cost surfaces stack: the Anthropic API and the GitHub Actions runner.

Anthropic API. Claude Sonnet 4.6 lists at $3 per million input tokens and $15 per million output tokens on Anthropic’s public pricing page 6 . A typical small PR diff is in the 1,000-to-5,000 token range; the system prompt adds a few hundred tokens; the JSON response runs 200-to-800 tokens. A median review at roughly 3,000 input tokens plus 500 output tokens costs about $0.0165, or under two US cents per review. A larger PR at 15,000 input tokens plus 1,500 output tokens runs around $0.0675, or under seven cents. The 60,000-character ceiling in the script keeps any single review from going much above ten cents even on a very large diff.

GitHub Actions runner minutes. GitHub gives every account a monthly free allowance of Linux runner minutes and storage 7 . Public repositories run Actions free with no minute cap. Private repositories burn minutes from the free monthly allowance; once consumed, Linux minutes are billed by the minute at GitHub’s per-minute rate published on the billing page (verify on the live page before relying on any specific cents-per-minute figure, since GitHub revises the rate periodically). This workflow typically runs in under 90 seconds, so even a private repo with heavy PR traffic stays well inside the free allowance for most small teams.

A team running, say, 200 PRs a month at a median review cost of two cents each spends roughly $3.30 per month on Anthropic API usage, plus negligible runner cost on a public repo or a fraction of the free private-repo allowance.

Security considerations

Six rules apply, in roughly decreasing order of how easy they are to get wrong.

1. Never put secrets into the model’s input. The script sends only the PR diff. It does not read environment variables into the prompt, does not concatenate os.environ into the user message, and does not fetch repository files outside the diff. The diff itself can contain user-controlled content (a contributor can submit a PR containing a malicious string), so the model’s response is treated as untrusted text on the way back out — see rule 4.

2. Use the minimum permissions: scope on GITHUB_TOKEN. The workflow above grants contents: read and pull-requests: write, nothing else. Workflows that default to the full token scope can write to issues, releases, deployments, and more; a prompt-injection bug that smuggles a malicious gh call into a step then has too much surface to play with 5 .

3. Pin third-party actions to a commit SHA, not a tag. The example uses actions/checkout@v4 and actions/setup-python@v5 for readability. In a production setup, replace each major-version tag with the specific commit SHA of the release the team audited. Tags can be moved; commit SHAs cannot. GitHub’s security-hardening guide spells out the supply-chain reasoning 8 .

4. Sandbox the model’s output before rendering. The format_comment function parses the model’s response as JSON. If parsing fails, the script renders the raw response inside a fenced code block, which makes Markdown and HTML in the output inert when GitHub renders the PR comment. Any text the model emits that looks like a slash command, an @mention of a user, or an inline image URL is, in the JSON path, embedded inside a controlled wrapper rather than appearing at the top level of the Markdown. This is a defence against prompt-injection content in the diff trying to manipulate either the reviewer or other readers of the PR comment.

5. Cap the diff size and the response token budget. The script truncates the diff at 60,000 characters and sets max_tokens=1500 on the Messages API call. Both caps bound cost and prevent a single PR from running away with the budget. A more sophisticated setup splits very large PRs into per-file reviews; for the tutorial path, truncation is the simpler safe default.

6. Treat the AI review as advisory. The Markdown footer on the posted comment names this explicitly. The workflow uses event: COMMENT on the review creation call, not APPROVE or REQUEST_CHANGES, so the bot never blocks a merge on its own opinion or hand-waves a human approval. Branch-protection rules that require human approval stay in force; the AI review is one more input, not a gate 9 .

GitHub Docs page on using encrypted secrets in workflows and protecting credentials from log exposure

Image: GitHub Docs page on using secrets in GitHub Actions, used for editorial coverage.

Going further

A handful of incremental upgrades are worth considering once the basic loop is working. Move from event: COMMENT to per-line review threads using the comments array on the create-review endpoint, which lets the bot attach suggestions to specific files and line numbers rather than a single summary comment. Split large diffs into per-file reviews to avoid the truncation ceiling. Add a label-based gate (if: contains(github.event.pull_request.labels.*.name, 'ai-review')) so the workflow only runs on PRs that explicitly request it. And consider migrating from the repo-secret pattern to OpenID Connect federation with a secret-manager-backed key if the team’s threat model justifies the extra setup cost.

The full primary-source references for every numerical claim and API surface name follow in the next section. The workflow above is the minimum reproducible path; the docs the references point to cover the next ninety percent.

How this article was made: an autonomous AI pipeline researched, drafted, fact-checked, and reviewed this piece, aggregating publicly-available information from the sources consulted below. AI (artificial intelligence) can make mistakes, so please cross-check the consulted sources before acting on anything here. Neural Tech Daily is not liable for decisions or outcomes based on this article.

Sources consulted

Cited Sources

  1. 1. Anthropic — Messages API endpoint reference (`POST /v1/messages`) (accessed )
  2. 2. Anthropic — Models overview and identifier list (accessed )
  3. 3. GitHub Docs — Using secrets in GitHub Actions (encrypted storage, runtime injection, log redaction) (accessed )
  4. 4. GitHub Docs — Workflow syntax reference (`on`, `jobs`, `steps`, `permissions`, `timeout-minutes`) (accessed )
  5. 5. GitHub Docs — `permissions:` key reference; minimum-scope guidance for `GITHUB_TOKEN` (accessed )
  6. 6. Anthropic — API pricing page (\$3 per million input tokens / \$15 per million output tokens for the Sonnet tier; prices fluctuate, verify before relying) (accessed )
  7. 7. GitHub Docs — About billing for GitHub Actions (free monthly minute allowance, per-minute billing past the allowance, public-repo no-cap rule) (accessed )
  8. 8. GitHub Docs — Security hardening for GitHub Actions (pin third-party actions to a commit SHA, restrict token scope, audit dependencies) (accessed )
  9. 9. GitHub REST API — Create a review for a pull request (`event` values: APPROVE / REQUEST_CHANGES / COMMENT) (accessed )

Further Reading

Anonymous · no cookies set

Report a problem with this article

Articles are produced by an autonomous AI pipeline; mistakes do happen. Tell us what's wrong and the editorial review will revisit the claim.

Category

Found this useful? Share it.