Run Claude Code headlessly in GitHub Actions to automate code review, generate PR descriptions, write tests, and build agentic CI/CD pipelines.
Three things required: Node.js in your runner, ANTHROPIC_API_KEY as a secret, and --dangerously-skip-permissions to bypass interactive prompts.
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code
- name: Run Claude Code Review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
DIFF=$(git diff origin/${{ github.base_ref }}...HEAD)
REVIEW=$(echo "$DIFF" | claude --print --dangerously-skip-permissions \
"Review this git diff for: bugs, security issues, performance problems, \
and missing error handling. Be concise. Format as markdown." 2>&1)
echo "$REVIEW" > /tmp/review.md
- name: Post Review as PR Comment
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const review = fs.readFileSync('/tmp/review.md', 'utf8');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: '## Claude Code Review\n\n' + review
});
- name: Generate PR Description
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
DIFF=$(git diff origin/${{ github.base_ref }}...HEAD --stat)
FULL_DIFF=$(git diff origin/${{ github.base_ref }}...HEAD)
DESC=$(echo "$FULL_DIFF" | claude --print --dangerously-skip-permissions \
"Write a clear PR description for this diff. Include: Summary (2-3 sentences), \
Changes (bullet list), and Testing (what to verify). Format as markdown.")
gh pr edit ${{ github.event.number }} --body "$DESC"
- name: Fix Lint Errors
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
# Run linter and capture failures
npm run lint 2>&1 > /tmp/lint-output.txt || true
if [ -s /tmp/lint-output.txt ]; then
cat /tmp/lint-output.txt | claude --dangerously-skip-permissions \
"Fix all the ESLint errors shown. Only fix what's reported — no other changes."
fi
- name: Commit fixes if any
run: |
git config user.email "ci@github-actions"
git config user.name "Claude Code Bot"
git diff --quiet || (git add -A && git commit -m "fix: auto-fix lint errors via Claude Code")
git push
- name: Generate Tests for New Code
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
# Find newly added functions in the diff
NEW_FUNCTIONS=$(git diff origin/${{ github.base_ref }}...HEAD \
--unified=0 | grep '^+.*function\|^+.*const.*=.*(' | head -20)
if [ -n "$NEW_FUNCTIONS" ]; then
echo "$NEW_FUNCTIONS" | claude --dangerously-skip-permissions \
"Generate Jest unit tests for each new function shown. \
Place tests in __tests__/ matching the source file structure. \
Include edge cases and error paths."
fi
| Model | Input price | Output price | Cost per PR review (~5K in / ~500 out) | Best for |
|---|---|---|---|---|
| Claude Haiku 4.5 | $0.80/MTok | $4/MTok | ~$0.006 | High-frequency, simple tasks (lint fix, commit msg) |
| Claude Sonnet 4.6 | $3/MTok | $15/MTok | ~$0.022 | Code review, test generation, complex refactors |
| Claude Opus 4.7 | $15/MTok | $75/MTok | ~$0.113 | Highest-stakes one-off tasks (architecture review) |
Use Haiku for triggered-on-every-commit jobs; use Sonnet for PR-level quality gates; reserve Opus for scheduled deep reviews.
Never hardcode ANTHROPIC_API_KEY in YAML files. Always use ${{ secrets.ANTHROPIC_API_KEY }}. Leaked keys can be used by anyone — Anthropic cannot refund API costs from key leaks.
pull-requests: write and contents: write if the job actually needs thempull_request (not push) to avoid running on every commit to mainif: github.event.pull_request.additions + github.event.pull_request.deletions < 2000 to avoid sending 50,000-token diffsgit diff origin/$BASE_BRANCH...HEAD --name-only to get the list of changed files, then pass them directly: claude --print --dangerously-skip-permissions "Review these files: $(git diff --name-only origin/main...HEAD | tr '\n' ' ')". You can also pipe the full diff: git diff origin/main...HEAD | claude --print .... For very large diffs, filter to specific file types first: git diff origin/main...HEAD -- '*.ts' '*.tsx'.