Using Claude Code interactively in the terminal is great — but at some point you want to go further: run it automatically when a PR is opened, have it analyze test failures without manual input, or wire it into a deployment check.
For all of these, you need Claude Code in headless (non-interactive) mode. The two flags that make this possible are --print and --output-format json. This guide shows how to use them in practice, with complete working code for GitHub Actions integration.
What --print Does
--print runs Claude Code in headless mode. Instead of launching the interactive UI, it takes a prompt, returns a response to stdout, and exits.
# Normal startup — opens interactive UI
claude
# With --print — runs headless, exits after responding
claude --print "Please review this code"
# Pipe file contents directly
cat src/auth.ts | claude --print "Identify any security issues"Without anything else, --print outputs plain text. That's sufficient for simple use cases, but when you need the output to drive downstream CI steps, structured JSON is much easier to work with.
Getting Structured Output with --output-format json
Combine --output-format json with --print to receive the response as JSON:
claude --print --output-format json "Please review this PR"Example output (formatted):
{
"type": "result",
"subtype": "success",
"result": "Review content here...",
"session_id": "sess_abc123",
"cost_usd": 0.0045,
"duration_ms": 2341,
"num_turns": 1
}The result field contains the response text. cost_usd gives you an approximate token cost — useful for tracking CI spending over time.
Automated Code Review in GitHub Actions
Here's a complete workflow that runs when a PR is opened, has Claude review the diff, and posts the result as a PR comment.
Prerequisites
# Repository secrets to configure:
# ANTHROPIC_API_KEY — your Anthropic API key
# GITHUB_TOKEN — provided automatically by ActionsWorkflow File
# .github/workflows/claude-review.yml
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
paths:
- "src/**"
- "lib/**"
jobs:
review:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code
- name: Get PR diff
id: diff
run: |
git diff origin/${{ github.base_ref }}...HEAD -- src/ lib/ > /tmp/pr_diff.txt
echo "diff_size=$(wc -c < /tmp/pr_diff.txt)" >> $GITHUB_OUTPUT
- name: Run Claude Review
id: claude
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
# Trim large diffs to avoid context limit issues
head -c 51200 /tmp/pr_diff.txt > /tmp/pr_diff_trimmed.txt
REVIEW=$(claude --print --output-format json \
--system "You are a senior engineer conducting a code review. Check for:
1. Security issues (SQL injection, XSS, auth bypass, etc.)
2. Performance issues (N+1 queries, unnecessary loops, memory leaks)
3. Potential bugs (missing error handling, edge cases)
4. Always acknowledge good implementation choices
If there are no issues, say so explicitly." \
"Please review the following PR diff:
$(cat /tmp/pr_diff_trimmed.txt)")
# Extract the result field from JSON
echo "$REVIEW" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if data.get('subtype') == 'success':
print(data['result'])
else:
print('Failed to generate review')
" > /tmp/review_comment.txt
echo "review_generated=true" >> $GITHUB_OUTPUT
- name: Post Review Comment
if: steps.claude.outputs.review_generated == 'true'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const reviewText = fs.readFileSync('/tmp/review_comment.txt', '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${reviewText}\n\n---\n*This review was generated automatically by Claude Code*`
});The workflow passes the diff to Claude via --system (for role context) and the prompt body (for the diff itself), extracts the JSON response, and posts it as a PR comment.
Analyzing Test Failures Automatically
A second practical use case: when tests fail in CI, have Claude generate root-cause hypotheses.
# .github/workflows/test-failure-analysis.yml
name: Test Failure Analysis
on:
workflow_run:
workflows: ["CI Tests"]
types: [completed]
jobs:
analyze:
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
runs-on: ubuntu-latest
permissions:
actions: read
issues: write
steps:
- uses: actions/checkout@v4
- name: Download test logs
uses: actions/download-artifact@v4
with:
name: test-results
path: /tmp/test-results/
continue-on-error: true
- name: Analyze with Claude
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
LOG_FILE="/tmp/test-results/test-output.txt"
if [ ! -f "$LOG_FILE" ]; then
echo "No test log found" > "$LOG_FILE"
fi
ANALYSIS=$(claude --print --output-format json \
"Here are test failure logs. List three probable root causes
and how to verify each one.
Log:
$(head -c 10000 $LOG_FILE)")
echo "$ANALYSIS" | python3 -c "
import json, sys
data = json.load(sys.stdin)
print(data.get('result', 'Analysis failed'))
" > /tmp/analysis.txt
cat /tmp/analysis.txtLocal Use: Shell Script Integration
You don't have to go straight to CI. A local shell script is a good starting point:
#!/bin/bash
# scripts/ai-review.sh
# Usage: ./scripts/ai-review.sh [base-branch]
BASE="${1:-main}"
DIFF=$(git diff "$BASE"...HEAD 2>/dev/null)
if [ -z "$DIFF" ]; then
echo "No diff found."
exit 0
fi
echo "Running Claude Code review..."
RESULT=$(echo "$DIFF" | claude --print --output-format json \
"Review this git diff. Focus on security and performance.")
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo "Error: Claude Code exited with code $EXIT_CODE"
exit 1
fi
echo "$RESULT" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('result', 'Failed to get result'))
cost = data.get('cost_usd', 0)
print(f'\n--- Cost: \${cost:.4f} USD ---')
except json.JSONDecodeError:
print('Failed to parse JSON')
sys.exit(1)
"Running this before committing catches issues that would otherwise only surface in code review.
Tracking Usage and Cost
Since --output-format json includes cost_usd, you can log CI spending systematically:
# scripts/track_claude_cost.py
import json
import sys
from datetime import datetime
def track_cost(json_output: str, run_name: str) -> None:
"""Log cost from Claude Code JSON output."""
try:
data = json.loads(json_output)
cost = data.get("cost_usd", 0)
tokens = data.get("usage", {})
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"run": run_name,
"cost_usd": cost,
"usage": tokens,
}
with open("claude_usage.jsonl", "a") as f:
f.write(json.dumps(log_entry) + "\n")
print(f"[{run_name}] cost: ${cost:.4f} USD")
except (json.JSONDecodeError, KeyError) as e:
print(f"Cost tracking failed: {e}", file=sys.stderr)
if __name__ == "__main__":
run_name = sys.argv[1] if len(sys.argv) > 1 else "unknown"
json_input = sys.stdin.read()
track_cost(json_input, run_name)Common Mistakes
① Using --output-format json without --print
--output-format only works in combination with --print. It doesn't function standalone.
# ❌ This won't work
claude --output-format json "question"
# ✅ Correct
claude --print --output-format json "question"② Sending oversized diffs
If input exceeds Claude's context window, you'll get errors or truncated responses. Trim to 50KB or split files before sending.
③ Rate limits under parallel CI runs
If ANTHROPIC_API_KEY has org-level usage limits, parallel CI jobs can hit rate limits. Issue a dedicated key for CI and set an appropriate usage cap.
Start with This Command
If you want to try it right now, paste this in your terminal:
# Have Claude review your current uncommitted changes
git diff HEAD | claude --print --output-format json \
"What stands out about this change?" | \
python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('result',''))"Once you have a feel for --print and --output-format json, wiring it into GitHub Actions is a straightforward next step. Start with the diff review workflow, get comfortable with the JSON parsing, and expand from there.