Skip to content

CI/CD & Automation

Claude Code can run non-interactively in CI/CD pipelines. This enables automated code review, PR descriptions, migrations, and more.

Two flags make Claude non-interactive:

Terminal window
# -p: Single prompt, exits after response
claude -p "explain this codebase"
# --print: Output only (no interactive UI)
claude --print -p "what does this function do"

Combine them for CI:

Terminal window
claude --print -p "review the changes in this PR for security issues"

Claude needs an API key in CI:

# GitHub Actions example
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

For headless operation, also set:

Terminal window
# Accept all permissions (use carefully)
claude --dangerously-skip-permissions -p "..."
# Or pre-approve specific tools in settings
name: Claude PR Review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code
- name: Review PR
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
claude --print -p "
Review the changes in this PR.
Focus on: bugs, security issues, performance.
Be concise. Output as markdown.
$(git diff origin/main...HEAD)
" > review.md
- name: Post Review Comment
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const review = fs.readFileSync('review.md', 'utf8');
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: review
});
name: Generate PR Description
on:
pull_request:
types: [opened]
jobs:
describe:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code
- name: Generate Description
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
claude --print -p "
Generate a PR description for these changes.
Include: summary, key changes, testing notes.
$(git log origin/main...HEAD --oneline)
$(git diff origin/main...HEAD --stat)
" > description.md
- name: Update PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const body = fs.readFileSync('description.md', 'utf8');
github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
body: body
});
name: Auto-fix Issues
on:
push:
branches: [main]
jobs:
fix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code
- name: Fix Linting Issues
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
# Run linter, capture issues
npm run lint 2>&1 | head -100 > lint-issues.txt || true
# Only run if there are issues
if [ -s lint-issues.txt ]; then
claude --dangerously-skip-permissions --print -p "
Fix these linting issues:
$(cat lint-issues.txt)
"
fi
- name: Commit Fixes
run: |
git config user.name "Claude Bot"
git config user.email "claude@example.com"
git add -A
git diff --staged --quiet || git commit -m "fix: auto-fix linting issues"
git push
Use CaseWhy It Works
PR reviewStateless, single pass
PR descriptionsSummarization task
Commit message generationSmall, focused input
Documentation updatesIsolated changes
Simple migrationsPattern-based transforms
Use CaseWhy It Fails
Complex debuggingNeeds iteration
Large refactorsRequires human judgment
Architectural decisionsNeeds context/discussion
Anything requiring /compactLong context management

Automated runs can burn through tokens fast:

# Use Haiku for simple tasks
- run: |
claude --print --model haiku -p "..."
# Limit agentic turns per run
- run: |
claude --print --max-turns 5 -p "..."
# Cache results where possible
- uses: actions/cache@v4
with:
path: .claude-cache
key: claude-${{ hashFiles('**/*.py') }}

For programmatic consumption:

Terminal window
# Request JSON output
claude --print -p "
Analyze this code and output JSON:
{
\"issues\": [{\"file\": string, \"line\": number, \"severity\": string, \"message\": string}],
\"summary\": string
}
$(cat src/main.py)
"
# Parse in CI
claude --print -p "..." | jq '.issues[] | select(.severity == "high")'

When Claude fails in CI:

# Capture full output
- run: |
claude --print -p "..." 2>&1 | tee claude-output.txt
# Upload as artifact
- uses: actions/upload-artifact@v4
if: failure()
with:
name: claude-debug
path: claude-output.txt