A code review bot that is terse.
A GitHub Action that reads the diff, opens the changed files, and posts a review focused on bugs, missing tests and security. No flattery, no nitpicks.
Most AI review bots leave forty comments per PR and your team starts ignoring them inside a week. This one is tuned the other way. It reads the diff and the surrounding code, but it only posts a comment if it has something genuinely useful to say. Empty reviews are fine. Silence is the goal half the time.
What it does
When a PR is opened or pushed, a GitHub Action fires. It pulls the unified diff from the GitHub API, fetches the changed files at the new SHA, and sends both to Claude with a prompt that says, in effect: only flag real problems. If a comment would make the author roll their eyes, do not write it.
The action then posts inline review comments on specific lines and a single summary comment at the top of the PR. If nothing is worth flagging, it posts nothing. The summary always includes a one-line verdict: ship, ship with edits, or block.
A small allowlist controls which paths get reviewed. Migrations, lockfiles and generated code are skipped. The whole thing fits in one workflow file plus a 200-line TypeScript script.
The problem it solves
Code review is the bottleneck on every team I have worked with. Senior engineers pile up review queues. Juniors wait days for feedback on a one-line typo. The fix is not to replace human review, it is to give the human a head start.
This bot takes the first pass: catches the obvious bugs, the missing null checks, the test that was written before the code change. The senior reviewer arrives to a PR that has already been pre-cleaned. Their time goes to architecture and intent, not catching that you forgot to await a promise.
Architecture
A single workflow, a single script, one Claude call per PR. The whole repo is fewer than 400 lines. It runs on the GitHub-hosted free tier; no self-hosted runner needed.
- →Trigger: pull_request opened, synchronize, ready_for_review.
- →Diff: pulled from the GitHub Compare API at the head SHA.
- →Context: the full text of every changed file at HEAD, capped at 200KB total.
- →Prompt: a strict reviewer persona, instructed to skip nits and flag only real issues.
- →Output: a JSON array of {file, line, severity, comment} plus a verdict.
- →Posting: GitHub Reviews API, one review per run, edits the previous one in place on subsequent pushes.
The interesting bits.
name: AI Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review]
jobs:
review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
working-directory: .github/review-bot
- run: node index.mjs
working-directory: .github/review-bot
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}import Anthropic from '@anthropic-ai/sdk'
import { Octokit } from '@octokit/rest'
const claude = new Anthropic()
const gh = new Octokit({ auth: process.env.GITHUB_TOKEN })
const [owner, repo] = process.env.REPO.split('/')
const pull_number = Number(process.env.PR_NUMBER)
const { data: files } = await gh.pulls.listFiles({ owner, repo, pull_number, per_page: 100 })
const reviewable = files.filter((f) =>
!/^(.*lock|dist\/|.*\.snap|.*\.generated\.)/.test(f.filename) &&
f.status !== 'removed' &&
f.changes < 800,
)
const context = reviewable.map((f) => `### ${f.filename}\n\n${f.patch ?? ''}`).join('\n\n')
const SYSTEM = `You review pull requests. Rules:
- Flag real problems only: bugs, missing error handling, security issues, broken tests.
- No style nits. No "consider renaming". No praise.
- If nothing is worth flagging, return an empty issues array.
- Verdict is one of: ship, ship_with_edits, block.`
const res = await claude.messages.create({
model: 'claude-sonnet-4-5',
max_tokens: 1500,
system: SYSTEM,
messages: [{ role: 'user', content: context }],
})
const { issues, verdict, summary } = JSON.parse(res.content[0].text)
if (issues.length === 0 && verdict === 'ship') process.exit(0)
await gh.pulls.createReview({
owner, repo, pull_number,
event: verdict === 'block' ? 'REQUEST_CHANGES' : 'COMMENT',
body: `**Verdict: ${verdict}**\n\n${summary}`,
comments: issues.map((i) => ({
path: i.file,
line: i.line,
body: `**${i.severity}**: ${i.comment}`,
})),
})Tools, picked deliberately.
From clone to working.
Drop in the workflow
Copy review.yml into .github/workflows. Copy the review-bot folder into .github.
Add the secret
Add ANTHROPIC_API_KEY in the repo settings. The GITHUB_TOKEN is provided by the runner.
Tune the allowlist
Edit the ignore regex to skip whatever your repo generates. Lockfiles, snapshots and dist directories by default.
Set the verdict policy
If you want block to actually block merges, add a branch protection rule that requires the AI Code Review check.
Open a noisy PR
Open a deliberately broken PR. The bot should flag the real bugs and stay silent on the formatting noise.
Iterate the prompt
After a week, read what it flagged and what it missed. Tune SYSTEM until silence feels honest.
Want early access?
This one is in the workshop. The pattern is documented above; the open source release is planned. Email me for a walkthrough or early access.