All Articles

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.

DevOpsBoysApr 13, 20266 min read
Share:Tweet

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:

  1. GitHub Actions triggers
  2. Gets the diff (changed files)
  3. Sends diff to Claude API with a DevOps-focused review prompt
  4. Posts review comments directly on the PR
  5. 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

  1. Go to console.anthropic.com
  2. Create API key
  3. Add to GitHub repo: Settings → Secrets → ANTHROPIC_API_KEY

Step 2: The Review Script

Create .github/scripts/ai-review.js:

javascript
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:

yaml
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.js

Example 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:

javascript
// 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

Newsletter

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

Comments