Code reviews are essential but time-consuming. What if every PR got an instant first pass from AI β catching bugs, suggesting improvements, and flagging security issues before a human reviewer even looks at it?
In this tutorial, weβll build a GitHub Action that automatically reviews pull requests using Claude. It reads the diff, analyzes the changes, and posts review comments directly on the PR. Your team gets faster feedback, and human reviewers can focus on architecture and design instead of catching typos and missing null checks.
What weβre building
When someone opens a PR, our bot:
- Reads the diff of changed files
- Sends each fileβs changes to Claude for review
- Posts inline comments on specific lines
- Adds a summary comment with an overall assessment
Step 1: Create the GitHub Action workflow
Create .github/workflows/ai-review.yml:
name: AI Code Review
on:
pull_request:
types: [opened, synchronize]
permissions:
contents: read
pull-requests: write
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm install @anthropic-ai/sdk @octokit/rest
- name: Run AI Review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: node .github/scripts/ai-review.mjs
Step 2: Add your Anthropic API key
Go to your repo β Settings β Secrets and variables β Actions β New repository secret:
- Name:
ANTHROPIC_API_KEY - Value: your Anthropic API key
GITHUB_TOKEN is automatically provided by GitHub Actions β no setup needed.
Step 3: Build the review script
Create .github/scripts/ai-review.mjs:
import Anthropic from '@anthropic-ai/sdk';
import { Octokit } from '@octokit/rest';
import { execSync } from 'child_process';
const anthropic = new Anthropic();
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const { PR_NUMBER, REPO_OWNER, REPO_NAME, BASE_SHA, HEAD_SHA } = process.env;
// Get the diff for this PR
function getDiff() {
return execSync(`git diff ${BASE_SHA}...${HEAD_SHA}`, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
}
// Parse diff into per-file chunks
function parseDiff(diff) {
const files = [];
const chunks = diff.split(/^diff --git /m).filter(Boolean);
for (const chunk of chunks) {
const fileMatch = chunk.match(/^a\/(.+?) b\//);
if (!fileMatch) continue;
const filename = fileMatch[1];
// Skip non-code files
if (/\.(lock|svg|png|jpg|ico|woff)$/.test(filename)) continue;
if (filename.includes('node_modules/')) continue;
files.push({ filename, patch: chunk.slice(0, 8000) }); // truncate large diffs
}
return files;
}
// Review a single file with AI
async function reviewFile(file) {
const message = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [{
role: 'user',
content: `Review this code diff. Focus on:
- Bugs and logic errors
- Security issues
- Performance problems
- Missing error handling
Be concise. Only comment on actual issues, not style preferences.
If the code looks good, respond with just "LGTM".
File: ${file.filename}
\`\`\`diff
${file.patch}
\`\`\``
}],
});
return message.content[0].text;
}
// Post a comment on the PR
async function postComment(body) {
await octokit.issues.createComment({
owner: REPO_OWNER,
repo: REPO_NAME,
issue_number: parseInt(PR_NUMBER),
body,
});
}
// Main
async function main() {
const diff = getDiff();
if (!diff.trim()) {
console.log('No diff found.');
return;
}
const files = parseDiff(diff);
if (files.length === 0) {
console.log('No reviewable files.');
return;
}
console.log(`Reviewing ${files.length} files...`);
// Review files (max 10 to control costs)
const reviews = [];
for (const file of files.slice(0, 10)) {
console.log(` Reviewing ${file.filename}...`);
const review = await reviewFile(file);
if (review.trim() !== 'LGTM') {
reviews.push({ filename: file.filename, review });
}
}
// Build the comment
let comment = '## π€ AI Code Review\n\n';
if (reviews.length === 0) {
comment += 'β
No issues found. LGTM!\n';
} else {
for (const { filename, review } of reviews) {
comment += `### \`${filename}\`\n\n${review}\n\n---\n\n`;
}
}
comment += `\n<sub>Reviewed ${files.length} file(s) with Claude. This is an automated review β human review is still recommended.</sub>`;
await postComment(comment);
console.log('Review posted!');
}
main().catch(err => {
console.error(err);
process.exit(1);
});
Step 4: Test it
Push the workflow to your repo and open a pull request. Within a minute or two, youβll see a comment from the bot with its review.
Tuning the prompt
The prompt is the most important part. Hereβs how to adjust it for your team:
Stricter reviews β add specific rules:
Also check for:
- Console.log statements that should be removed
- TODO comments without issue links
- Functions longer than 50 lines
Framework-specific β add context:
This is a Next.js project using the App Router with TypeScript.
Check for proper use of 'use client' directives and server/client component boundaries.
Ignore certain patterns:
Ignore test files. Don't comment on import ordering.
Controlling costs
Each file review costs roughly $0.005-0.02. A typical PR with 5 files costs about $0.05-0.10. To keep costs down:
- The script limits reviews to 10 files max
- Lock files, images, and
node_modulesare skipped - Large diffs are truncated to 8,000 characters per file
- Use
claude-haikuinstead ofclaude-sonnetfor cheaper reviews (less accurate but 10x cheaper)
For a team of 10 developers doing 5 PRs/day each, expect roughly $25-50/month.
Making it smarter
Only review changed lines
Instead of reviewing the entire file diff, extract just the added lines:
const addedLines = file.patch
.split('\n')
.filter(line => line.startsWith('+') && !line.startsWith('+++'))
.join('\n');
Skip draft PRs
on:
pull_request:
types: [opened, synchronize, ready_for_review]
jobs:
review:
if: github.event.pull_request.draft == false
Add a label to skip review
jobs:
review:
if: "!contains(github.event.pull_request.labels.*.name, 'skip-ai-review')"
What you learned
- How to build a GitHub Action that reacts to pull requests
- How to parse git diffs programmatically
- How to use the Anthropic SDK in a CI/CD context
- How to post comments via the GitHub API with Octokit
The bot wonβt replace human reviewers, but it catches the obvious stuff β missing error handling, potential null pointers, security issues β so your team can focus on the harder questions during review.
Related resources
- GitHub Actions cheat sheet β workflow syntax reference
- Git complete guide β understanding diffs and branches
- What is CI/CD? β the automation pattern behind this