Build an AI GitHub PR Review Bot with Claude API (2026)
Build a GitHub Actions workflow that automatically reviews every pull request using Claude AI ā catches bugs, security issues, and bad patterns before human review.
Code review is the bottleneck in most teams. An AI reviewer catches obvious bugs, security issues, and style violations instantly ā freeing human reviewers for architecture discussions. Here's how to build one with Claude API and GitHub Actions.
What It Does
On every PR:
- GitHub Actions triggers
- Gets the diff (changed files)
- Sends diff to Claude API with a DevOps-focused review prompt
- Posts review comments directly on the PR
- Requests changes if critical issues found, approves if clean
Architecture
PR opened/updated
ā
GitHub Actions workflow
ā
Fetch PR diff via GitHub API
ā
Claude API (analyze diff)
ā
Post review comments via GitHub API
ā
Request Changes / Approve / Comment
Step 1: Get Claude API Key
- Go to console.anthropic.com
- Create API key
- Add to GitHub repo: Settings ā Secrets ā
ANTHROPIC_API_KEY
Step 2: The Review Script
Create .github/scripts/ai-review.js:
const https = require('https')
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY
const GITHUB_TOKEN = process.env.GITHUB_TOKEN
const REPO = process.env.GITHUB_REPOSITORY // owner/repo
const PR_NUMBER = process.env.PR_NUMBER
const BASE_URL = 'https://api.github.com'
// Fetch PR diff
async function getPRDiff() {
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.github.com',
path: `/repos/${REPO}/pulls/${PR_NUMBER}`,
headers: {
'Authorization': `token ${GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3.diff',
'User-Agent': 'AI-PR-Reviewer'
}
}
let data = ''
https.get(options, res => {
res.on('data', chunk => data += chunk)
res.on('end', () => resolve(data))
}).on('error', reject)
})
}
// Get PR files for targeted comments
async function getPRFiles() {
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.github.com',
path: `/repos/${REPO}/pulls/${PR_NUMBER}/files`,
headers: {
'Authorization': `token ${GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'AI-PR-Reviewer'
}
}
let data = ''
https.get(options, res => {
res.on('data', chunk => data += chunk)
res.on('end', () => resolve(JSON.parse(data)))
}).on('error', reject)
})
}
// Call Claude API
async function reviewWithClaude(diff, files) {
const fileList = files.map(f => `${f.status}: ${f.filename}`).join('\n')
const prompt = `You are a senior DevOps engineer reviewing a pull request. Analyze this diff and provide a thorough code review.
Changed files:
${fileList}
Diff:
\`\`\`diff
${diff.slice(0, 15000)}
\`\`\`
Review for:
1. **Security issues** ā hardcoded secrets, insecure configs, exposed ports, missing auth
2. **Kubernetes best practices** ā resource limits, health checks, security contexts, RBAC
3. **Docker best practices** ā non-root user, image pinning, layer optimization
4. **CI/CD issues** ā missing error handling, insecure workflows, missing tests
5. **Terraform issues** ā missing state locking, no prevent_destroy on critical resources
6. **Performance issues** ā inefficient queries, missing caching, no rate limits
7. **General bugs** ā logic errors, typos in config keys, wrong port numbers
Format your response as JSON:
{
"verdict": "APPROVE" | "REQUEST_CHANGES" | "COMMENT",
"summary": "2-3 sentence overall assessment",
"issues": [
{
"severity": "critical" | "high" | "medium" | "low",
"file": "path/to/file",
"description": "Clear description of the issue",
"suggestion": "How to fix it"
}
],
"positives": ["What was done well"]
}`
return new Promise((resolve, reject) => {
const body = JSON.stringify({
model: 'claude-opus-4-6',
max_tokens: 2000,
messages: [{ role: 'user', content: prompt }]
})
const options = {
hostname: 'api.anthropic.com',
path: '/v1/messages',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01',
'Content-Length': Buffer.byteLength(body)
}
}
let data = ''
const req = https.request(options, res => {
res.on('data', chunk => data += chunk)
res.on('end', () => {
const response = JSON.parse(data)
try {
const text = response.content[0].text
// Extract JSON from response
const jsonMatch = text.match(/\{[\s\S]*\}/)
resolve(JSON.parse(jsonMatch[0]))
} catch (e) {
resolve({ verdict: 'COMMENT', summary: response.content[0].text, issues: [], positives: [] })
}
})
})
req.on('error', reject)
req.write(body)
req.end()
})
}
// Post review to GitHub
async function postReview(review) {
const issuesList = review.issues.map(issue => {
const emoji = issue.severity === 'critical' ? 'šØ' :
issue.severity === 'high' ? 'ā ļø' :
issue.severity === 'medium' ? 'š' : 'ā¹ļø'
return `${emoji} **${issue.severity.toUpperCase()}** ā \`${issue.file}\`\n> ${issue.description}\n\nš” **Fix:** ${issue.suggestion}`
}).join('\n\n---\n\n')
const positivesList = review.positives.map(p => `ā
${p}`).join('\n')
const body = `## š¤ AI Code Review
### Summary
${review.summary}
${review.issues.length > 0 ? `### Issues Found\n\n${issuesList}` : '### No Issues Found š'}
${review.positives.length > 0 ? `### What's Good\n${positivesList}` : ''}
---
*Reviewed by Claude AI ā [DevOpsBoys](https://devopsboys.com)*`
return new Promise((resolve, reject) => {
const bodyData = JSON.stringify({
body,
event: review.verdict, // APPROVE, REQUEST_CHANGES, or COMMENT
})
const options = {
hostname: 'api.github.com',
path: `/repos/${REPO}/pulls/${PR_NUMBER}/reviews`,
method: 'POST',
headers: {
'Authorization': `token ${GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json',
'User-Agent': 'AI-PR-Reviewer',
'Content-Length': Buffer.byteLength(bodyData)
}
}
const req = https.request(options, res => {
let data = ''
res.on('data', chunk => data += chunk)
res.on('end', () => resolve(JSON.parse(data)))
})
req.on('error', reject)
req.write(bodyData)
req.end()
})
}
// Main
async function main() {
console.log(`Reviewing PR #${PR_NUMBER} in ${REPO}`)
const diff = await getPRDiff()
const files = await getPRFiles()
if (!diff || diff.length < 10) {
console.log('No diff found, skipping review')
return
}
console.log(`Diff size: ${diff.length} chars, ${files.length} files changed`)
const review = await reviewWithClaude(diff, files)
console.log('Review verdict:', review.verdict)
console.log('Issues found:', review.issues.length)
await postReview(review)
console.log('Review posted successfully')
// Fail CI if critical issues found
const criticalIssues = review.issues.filter(i => i.severity === 'critical')
if (criticalIssues.length > 0) {
console.error(`${criticalIssues.length} critical issues found`)
process.exit(1)
}
}
main().catch(err => {
console.error('Error:', err)
process.exit(1)
})Step 3: GitHub Actions Workflow
Create .github/workflows/ai-review.yml:
name: AI PR Review
on:
pull_request:
types: [opened, synchronize, reopened]
# Optional: only review specific paths
# paths:
# - 'src/**'
# - 'Dockerfile'
# - '*.tf'
# - '.github/workflows/**'
jobs:
review:
runs-on: ubuntu-latest
# Only review PRs, not internal pushes
if: github.event.pull_request.head.repo.full_name == github.repository
permissions:
contents: read
pull-requests: write # needed to post review
steps:
- uses: actions/checkout@v4
- name: Run AI Review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: node .github/scripts/ai-review.jsExample Review Output
When a PR contains a hardcoded secret in a Dockerfile:
## š¤ AI Code Review
### Summary
This PR adds a new service deployment but contains a critical security issue ā
a hardcoded API key in the Dockerfile. 2 medium issues also need attention.
### Issues Found
šØ CRITICAL ā `Dockerfile`
> Line 12: ENV API_KEY=sk-1234abcdsecret hardcodes a secret in the image layer.
> Anyone who pulls this image can extract the key with `docker history`.
š” Fix: Remove the ENV line. Pass secrets at runtime via Kubernetes Secrets or
environment variables from your secret manager.
ā ļø HIGH ā `k8s/deployment.yaml`
> No resource limits defined. Pod can consume unlimited CPU/memory,
> causing node-level resource starvation.
š” Fix: Add resources.limits.cpu and resources.limits.memory to the container spec.
### What's Good
ā
Multi-stage build correctly separates build and runtime stages
ā
Non-root USER configured in final stage
Cost Estimate
- Claude claude-opus-4-6: ~$0.015 per 1K input tokens
- Average PR diff: ~3K tokens
- Cost per review: ~$0.05
- 100 PRs/month = ~$5/month
Use claude-haiku-4-5-20251001 for 10x cheaper reviews on smaller PRs: model: 'claude-haiku-4-5-20251001'
Customize the Prompt
The prompt is the most powerful lever. Tailor it to your stack:
// For infrastructure-focused repos
const prompt = `Review this Terraform/K8s diff for:
- Missing state locking
- No prevent_destroy on databases
- Public S3 buckets
- Security groups allowing 0.0.0.0/0
- Missing encryption at rest
...`
// For application repos
const prompt = `Review this Node.js/Python diff for:
- SQL injection vulnerabilities
- Missing input validation
- Exposed error messages with stack traces
- Hardcoded credentials
...`Resources
- Claude API Docs ā API reference and model selection
- GitHub Actions Docs ā workflow syntax reference
- KodeKloud CI/CD Path ā hands-on GitHub Actions labs
Today I Fixed
Short real fixes from production ā posted daily
Stay ahead of the curve
Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam ā just practical engineering content.
Related Articles
Build an AI AWS Security Auditor with Claude API and Boto3
Use Python, boto3, and the Claude API to automatically audit your AWS environment for security misconfigurations and get AI-powered remediation recommendations.
Build an AI DevOps Daily Digest with Claude API
Build a Python script that collects pending PRs, firing Prometheus alerts, Kubernetes warnings, and failed CI jobs, then uses Claude API to generate a prioritized morning briefing posted to Slack.
Build an AI-Powered Dockerfile Security Scanner with Claude
Build a tool that scans Dockerfiles for security issues using Claude API ā finds hardcoded secrets, root users, unscanned base images, and missing security best practices.