Chapter 12: CI/CD & GitHub Actions
Learning Objectives
- Set up
claude-code-actionfor automated PR review with copy-paste YAML configs - Configure automatic issue triage with labels, priority, and assignment
- Build a CI security gate that blocks merges when Claude finds vulnerabilities
- Integrate Claude into existing CI pipelines alongside your test suite
- Handle API keys, rate limits, and cost controls in CI environments
Appendix links: A05 Tool Calling Internals explains how Claude selects tools and processes JSON Schema definitions, which is the same mechanism powering
claude -pin CI scripts. See also A12 Safety & Alignment for how permission bypass in CI requires compensating security controls.
How Claude Code Works in CI
There are two integration points:
- claude-code-action -- A GitHub Action that triggers Claude on PRs and issues. It runs Claude in the cloud, not on your runner
claude -pin scripts -- Run Claude as a CLI tool in any CI pipeline (GitHub Actions, GitLab CI, Jenkins, CircleCI). Runs on the CI runner itself
GitHub Event (PR opened, issue created, push)
|
v
+-------------------------------+
| claude-code-action | Cloud-based, managed by Anthropic
| - Reviews PR diffs | Needs: ANTHROPIC_API_KEY
| - Posts review comments | Cost: per-token
| - Creates PRs from issues |
+-------------------------------+
or
+-------------------------------+
| claude -p in CI script | Runs on your CI runner
| - Custom analysis | Needs: ANTHROPIC_API_KEY + runner setup
| - Quality gates | Cost: per-token + runner minutes
| - Security scans |
+-------------------------------+Demo 31: Complete GitHub Actions PR Review
The YAML Config
Create .github/workflows/claude-review.yml:
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, reopened]
issue_comment:
types: [created]
# Required: write permissions for PR comments
permissions:
contents: read
pull-requests: write
issues: write
jobs:
review:
# Only run on PR events, or on issue comments that mention @claude
if: |
github.event_name == 'pull_request' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '@claude'))
runs-on: ubuntu-latest
steps:
- name: Run Claude Code Review
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Optional: customize what Claude focuses on
review_instructions: |
Focus your review on:
1. Security vulnerabilities (SQL injection, XSS, auth bypass)
2. Error handling gaps (unhandled promises, missing try/catch)
3. Performance issues (N+1 queries, missing indexes, memory leaks)
4. Type safety (any casts, missing null checks)
Do NOT comment on:
- Code style or formatting (handled by linters)
- Import ordering
- Minor naming preferences
For each issue found, rate severity: critical / warning / suggestion
# Optional: limit the model to control costs
model: claude-sonnet-4-6
# Optional: set max tokens to prevent runaway costs
max_tokens: 16384What This Does
When a PR is opened or updated:
- Claude reads the PR diff and all changed files
- Claude analyzes the changes against the review instructions
- Claude posts inline review comments on specific lines
- Claude posts a summary comment with the overall assessment
When someone comments "@claude can you also check the error handling in the payment module?":
- Claude reads the comment and the PR context
- Claude performs the requested additional analysis
- Claude responds as a reply to the comment
What the PR review looks like on GitHub:
GitHub PR #203: "Add user search endpoint"
claude-code-action (bot) reviewed 3 minutes ago
src/api/users.ts line 42:
+---------------------------------------------------------------+
| WARNING: SQL injection risk |
| |
| The search query is interpolated directly into the SQL string: |
| const results = await db.query( |
| `SELECT * FROM users WHERE name LIKE '%${query}%'` |
| ); |
| |
| Use parameterized queries instead: |
| const results = await db.query( |
| 'SELECT * FROM users WHERE name LIKE $1', |
| [`%${query}%`] |
| ); |
+---------------------------------------------------------------+
src/api/users.ts line 58:
+---------------------------------------------------------------+
| SUGGESTION: Missing error handling |
| |
| The database query has no try/catch. If the query fails, |
| the error will propagate as an unhandled promise rejection. |
| Wrap in try/catch and return a proper 500 response. |
+---------------------------------------------------------------+
Summary:
+---------------------------------------------------------------+
| Found 1 warning and 1 suggestion in 3 files scanned. |
| |
| The SQL injection on line 42 should be fixed before merging. |
| The missing error handling is a good practice improvement. |
+---------------------------------------------------------------+What Just Happened?
Storing Your API Key
# In your repository settings:
# Settings > Secrets and variables > Actions > New repository secret
# Name: ANTHROPIC_API_KEY
# Value: sk-ant-...Demo 32: Auto-Triage Incoming Issues
The YAML Config
Create .github/workflows/claude-triage.yml:
name: Claude Issue Triage
on:
issues:
types: [opened]
permissions:
contents: read
issues: write
jobs:
triage:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Triage with Claude
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
model: claude-sonnet-4-6
# Claude reads the issue and the codebase, then triages
direct_prompt: |
Analyze this GitHub issue and triage it.
Issue title: ${{ github.event.issue.title }}
Issue body: ${{ github.event.issue.body }}
Steps:
1. Read the issue carefully
2. Search the codebase to understand if this is a real bug,
a feature request, or a question
3. Determine:
- Type: bug | feature | question | documentation
- Priority: P0-critical | P1-high | P2-medium | P3-low
- Component: which part of the codebase is affected
- Assignee suggestion: based on git blame of affected files
4. Add appropriate labels using the GitHub MCP tools
5. If it is a bug, check if there is an obvious fix and mention it
6. Post a comment summarizing the triage decision
Label mapping:
- Type labels: bug, enhancement, question, documentation
- Priority labels: P0-critical, P1-high, P2-medium, P3-low
- Component labels: api, frontend, database, auth, infra
allowed_tools: |
Read
Glob
Grep
Bash(git log *)
Bash(git blame *)
mcp__github__*What This Looks Like in Practice
Someone opens an issue:
Title: Login fails when email contains a plus sign
Body: When I try to log in with
john+test@gmail.com, I get a 400 error. This worked last week. Using Chrome 124 on macOS.
Within 2 minutes, Claude:
- Searches the codebase for email validation logic
- Finds the regex in
src/auth/validators.tsthat does not handle+ - Checks
git logto see this regex was changed 3 days ago in PR #189 - Adds labels:
bug,P1-high,auth - Posts a comment:
GitHub Issue #210: "Login fails when email contains a plus sign"
Labels added: bug, P1-high, auth
claude-code-action (bot) commented 2 minutes ago:
+---------------------------------------------------------------+
| Triage Summary |
| |
| Type: Bug | Priority: P1-high | Component: auth |
| |
| The email validation regex in src/auth/validators.ts:42 was |
| updated in PR #189 (3 days ago) and no longer allows + in the |
| local part of email addresses. |
| |
| Current regex: ^[a-zA-Z0-9.]+@ |
| Should be: ^[a-zA-Z0-9.+]+@ |
| |
| Suggested assignee: @alice (last modified this file) |
| |
| This is likely a quick fix. The regex change in PR #189 was |
| intended to block special characters for security, but + is a |
| valid RFC 5321 character in email local parts. |
+---------------------------------------------------------------+What Just Happened?
Demo 33: CI Security Gate
The Goal
Add a CI check that runs on every PR and blocks the merge if Claude identifies security vulnerabilities in the changed code. This runs alongside your existing test suite.
The YAML Config
Create .github/workflows/claude-security.yml:
name: Claude Security Scan
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Need full history for diff
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code
- name: Get changed files
id: changed
run: |
FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD \
| grep -E '\.(ts|tsx|js|jsx|py|go|rs|java)$' \
| head -50)
echo "files=$FILES" >> $GITHUB_OUTPUT
- name: Run Security Scan
id: scan
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
cat > /tmp/security-prompt.txt << 'PROMPT'
You are a security auditor. Analyze the following code changes for
security vulnerabilities.
Check for:
1. SQL injection (string concatenation in queries)
2. XSS (unescaped user input in HTML/JSX)
3. Authentication bypass (missing auth checks on endpoints)
4. Path traversal (unsanitized file paths from user input)
5. Secrets in code (hardcoded API keys, passwords, tokens)
6. Insecure deserialization
7. SSRF (user-controlled URLs in server-side requests)
8. Command injection (user input in shell commands)
For each vulnerability found, output a JSON object:
{
"file": "path/to/file.ts",
"line": 42,
"severity": "critical|high|medium|low",
"type": "sql-injection",
"description": "User input concatenated into SQL query without parameterization",
"suggestion": "Use parameterized queries: db.query('SELECT * FROM users WHERE id = $1', [userId])"
}
If no vulnerabilities are found, output:
{"status": "clean", "files_scanned": 5}
Output ONLY valid JSON (one object per line, no other text).
PROMPT
# Get the diff and pipe it to Claude
git diff origin/${{ github.base_ref }}...HEAD -- ${{ steps.changed.outputs.files }} \
| claude -p "$(cat /tmp/security-prompt.txt)" \
--permission-mode bypassPermissions \
--output-format json \
--max-tokens 8192 \
> /tmp/security-results.json
# Check for critical or high severity findings
if grep -q '"severity": "critical"' /tmp/security-results.json; then
echo "has_critical=true" >> $GITHUB_OUTPUT
else
echo "has_critical=false" >> $GITHUB_OUTPUT
fi
if grep -q '"severity": "high"' /tmp/security-results.json; then
echo "has_high=true" >> $GITHUB_OUTPUT
else
echo "has_high=false" >> $GITHUB_OUTPUT
fi
- name: Post Results as PR Comment
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const results = fs.readFileSync('/tmp/security-results.json', 'utf8');
let body = '## Security Scan Results\n\n';
const lines = results.trim().split('\n').filter(l => l.trim());
const findings = lines.map(l => {
try { return JSON.parse(l); } catch { return null; }
}).filter(Boolean);
if (findings.length === 1 && findings[0].status === 'clean') {
body += 'No security vulnerabilities found. Files scanned: ' +
findings[0].files_scanned + '\n';
} else {
body += '| Severity | File | Line | Type | Description |\n';
body += '|----------|------|------|------|-------------|\n';
for (const f of findings) {
if (f.severity) {
const icon = f.severity === 'critical' ? '🔴' :
f.severity === 'high' ? '🟠' :
f.severity === 'medium' ? '🟡' : '🟢';
body += `| ${icon} ${f.severity} | ${f.file} | ${f.line} | ` +
`${f.type} | ${f.description} |\n`;
}
}
body += '\n---\n';
for (const f of findings) {
if (f.suggestion) {
body += `\n**${f.file}:${f.line}** - ${f.suggestion}\n`;
}
}
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
- name: Fail on Critical Findings
if: steps.scan.outputs.has_critical == 'true'
run: |
echo "CRITICAL security vulnerabilities found. Blocking merge."
echo "Review the PR comment for details."
exit 1
- name: Warn on High Findings
if: steps.scan.outputs.has_high == 'true'
run: |
echo "::warning::HIGH severity security findings detected. Review recommended before merge."What the CI Output Looks Like
When vulnerabilities are found (merge blocked):
GitHub Actions: Claude Security Scan
Run Security Scan .............. 45s
Scanning 8 changed files...
Analyzing diff against security checklist...
Post Results as PR Comment ..... 2s
Posted security scan results as PR comment
Fail on Critical Findings ...... FAILED
CRITICAL security vulnerabilities found. Blocking merge.
Review the PR comment for details.
========================================
Job failed. 1 critical finding must be resolved before merge.The PR comment posted by the security gate:
GitHub PR #215: "Add admin API endpoints"
github-actions (bot) commented 1 minute ago:
## Security Scan Results
| Severity | File | Line | Type | Description |
|----------|----------------|------|----------------|--------------------------------|
| CRITICAL | src/admin.ts | 23 | auth-bypass | Admin endpoint has no auth |
| | | | | middleware check |
| HIGH | src/admin.ts | 45 | sql-injection | User input in query string |
| MEDIUM | src/admin.ts | 67 | missing-rate | No rate limiting on admin |
| | | | | endpoints |
---
src/admin.ts:23 - Add authentication middleware:
router.use('/admin', authMiddleware, adminRouter)
src/admin.ts:45 - Use parameterized queries:
db.query('SELECT * FROM users WHERE role = $1', [role])
src/admin.ts:67 - Add rate limiting:
router.use('/admin', rateLimit({ windowMs: 15*60*1000, max: 100 }))When the scan is clean:
GitHub Actions: Claude Security Scan
Run Security Scan .............. 32s
Scanning 3 changed files...
Analyzing diff against security checklist...
Post Results as PR Comment ..... 2s
## Security Scan Results
No security vulnerabilities found. Files scanned: 3What Just Happened?
Making It a Required Check
In your GitHub repository settings:
- Go to Settings, then Branches, then Branch protection rules
- Edit (or create) the rule for
main - Enable "Require status checks to pass before merging"
- Search for "Claude Security Scan" and add it as a required check
Now PRs with critical security vulnerabilities cannot be merged until the issues are resolved.
Cost Control
The security scan uses claude -p (pipe mode) with the diff as input. This is efficient:
- Only the changed code is analyzed, not the entire codebase
--max-tokens 8192caps the response cost- Sonnet is used by default (cheaper than Opus)
- Average cost per PR: $0.02-0.15 depending on diff size
For large repositories, add a file count limit:
# Only scan the first 50 changed files
FILES=$(git diff --name-only origin/main...HEAD | head -50)Combining All Three: A Complete CI Pipeline
Here is how the three workflows fit together:
# .github/workflows/claude-ci.yml
name: Claude CI Pipeline
on:
pull_request:
types: [opened, synchronize, reopened]
issues:
types: [opened]
issue_comment:
types: [created]
permissions:
contents: read
pull-requests: write
issues: write
jobs:
# Job 1: Review PRs
pr-review:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
model: claude-sonnet-4-6
review_instructions: |
Focus on correctness, security, and error handling.
Skip style comments.
# Job 2: Security gate (blocks merge on critical findings)
security-gate:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: npm install -g @anthropic-ai/claude-code
- run: |
git diff origin/${{ github.base_ref }}...HEAD \
| claude -p "Scan for security vulnerabilities. Output JSON." \
--permission-mode bypassPermissions \
| tee /tmp/results.json
grep -q '"severity": "critical"' /tmp/results.json && exit 1 || true
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# Job 3: Triage new issues
issue-triage:
if: github.event_name == 'issues'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
model: claude-sonnet-4-6
direct_prompt: |
Triage this issue. Add type and priority labels.
Issue: ${{ github.event.issue.title }}
Body: ${{ github.event.issue.body }}
# Job 4: Respond to @claude mentions in PR comments
claude-assist:
if: |
github.event_name == 'issue_comment' &&
contains(github.event.comment.body, '@claude')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}What the combined pipeline looks like in the GitHub Actions tab:
GitHub Actions: Claude CI Pipeline
Triggered by: pull_request (opened)
pr-review ................. PASSED (1m 23s)
Claude reviewed 5 files, posted 2 inline comments
security-gate ............. PASSED (48s)
No critical vulnerabilities found. 5 files scanned.
issue-triage .............. SKIPPED
(Only runs on issue events)
claude-assist ............. SKIPPED
(Only runs on @claude mentions)When Things Go Wrong
CI Token Permissions Insufficient
Symptom: The workflow fails with a 403 error when trying to post PR comments or add labels.
GitHub Actions: Claude Code Review
Run Claude Code Review ........ FAILED
Error: Resource not accessible by integration
HttpError: 403 - Resource not accessible by integration
This usually means the GITHUB_TOKEN does not have sufficient permissions.Root cause: The permissions block in your workflow YAML is missing or incomplete. GitHub Actions uses a restricted token by default.
Fix: Add the required permissions to your workflow:
permissions:
contents: read # Read repo files and diffs
pull-requests: write # Post PR review comments
issues: write # Add labels, post issue commentsIf you are using a fine-grained personal access token instead of the default GITHUB_TOKEN, ensure it has:
- Repository access: Contents (read), Pull requests (write), Issues (write)
- For organization repos: the token must be approved by an org admin
API Rate Limiting
Symptom: The security scan or review fails intermittently with rate limit errors.
Run Security Scan .............. FAILED
Error: 429 Too Many Requests
Rate limit exceeded. Please retry after 60 seconds.
Your organization has sent too many requests to the Anthropic API.
Current limit: 1000 requests per minute.Root cause: Multiple PRs opened simultaneously, or the workflow is re-triggered on every push to a PR branch, exhausting your API rate limit.
Fix -- reduce trigger frequency:
on:
pull_request:
types: [opened, reopened] # Remove 'synchronize' to avoid re-running on every push
paths:
- 'src/**' # Only trigger on source code changes
- '!src/**/*.test.ts' # Skip test-only changesFix -- add concurrency control:
concurrency:
group: claude-review-${{ github.event.pull_request.number }}
cancel-in-progress: true # Cancel previous runs when new commits are pushedFix -- add retry logic for transient failures:
- name: Run Security Scan (with retry)
uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 3
retry_wait_seconds: 60
command: |
git diff origin/${{ github.base_ref }}...HEAD \
| claude -p "Scan for security vulnerabilities." \
--permission-mode bypassPermissions \
> /tmp/security-results.jsonCost Control Failures
Symptom: Your monthly Anthropic API bill is unexpectedly high due to CI usage.
Anthropic Dashboard:
─────────────────────────────────
Usage this month: $847.32
Budget limit: $200.00
Top consumers:
claude-code-action (PR review): $312.00 (2,400 runs)
claude -p (security scan): $289.00 (2,400 runs)
claude -p (issue triage): $246.32 (1,800 runs)Root causes and fixes:
- No max_tokens cap: Without
--max-tokens, Claude may generate very long responses for large diffs.
# Always set max_tokens in CI
max_tokens: 8192 # For claude-code-action
--max-tokens 8192 # For claude -p- Using Opus instead of Sonnet: Opus costs roughly 15x more per token than Sonnet. For automated reviews, Sonnet is usually sufficient.
model: claude-sonnet-4-6 # Not claude-opus-4-7- Running on every push to a PR: If a developer pushes 10 commits to a PR branch, the review runs 10 times.
# Use concurrency to cancel previous runs
concurrency:
group: claude-${{ github.event.pull_request.number }}
cancel-in-progress: true- No file filtering: Scanning documentation or config changes wastes tokens.
# Only scan source code files
FILES=$(git diff --name-only origin/main...HEAD \
| grep -E '\.(ts|tsx|js|jsx|py|go|rs|java)$' \
| head -50)- Set a budget alert: Configure Anthropic's API dashboard to alert you when spending exceeds a threshold.
Cost estimation formula:
Cost per PR review = (input_tokens + output_tokens) * price_per_token
Average PR diff: ~2,000 tokens input
Average review: ~1,000 tokens output
Sonnet pricing: ~$0.003 per 1K input, ~$0.015 per 1K output
Cost per review: ~$0.006 + ~$0.015 = ~$0.02
100 PRs/week = ~$2/week = ~$8/month (review only)
Add security scan: ~$16/month total
Add issue triage: ~$24/month totalANTHROPIC_API_KEY Secret Not Found
Symptom: The workflow fails immediately because the API key is not available.
Run Claude Code Review ........ FAILED
Error: ANTHROPIC_API_KEY is not set.
Please add your API key as a repository secret.Fix:
# 1. Go to your repository on GitHub
# 2. Settings > Secrets and variables > Actions
# 3. Click "New repository secret"
# 4. Name: ANTHROPIC_API_KEY
# 5. Value: your Anthropic API key (starts with sk-ant-)For organization repositories: The secret must be set at the organization level or the repository must be granted access to the organization secret.
For forked repositories: GitHub Actions does not pass secrets to workflows triggered by pull requests from forks. This is a security feature. Use pull_request_target instead of pull_request if you need to review fork PRs, but be aware of the security implications.
Knowledge Check
Chapter Summary
claude-code-actionhandles PR review and issue triage with minimal configuration -- drop the YAML in and it worksclaude -pin CI scripts gives you full control for custom analysis and quality gates- Security gates can block merges by exiting with code 1 on critical findings
- Cost control: use Sonnet, cap max tokens, add concurrency control, and only analyze changed files (not the full repo)
- All three modes (review, triage, @-mention responses) can run from a single workflow file
- CI token permissions must be explicitly declared in the workflow YAML
- Rate limiting and cost spikes are the most common operational issues -- add concurrency control and budget alerts from day one
Further reading: A05 Tool Calling Internals explains the JSON Schema tool selection mechanism that powers
claude -pin CI. A12 Safety & Alignment discusses whybypassPermissionsin CI requires compensating controls like restricted runner environments and audit logging.
Next: Chapter 13: Agent SDK