The gist
In regular mode, Claude Code is a surgeon with an assistant (you): it proposes, you approve. In headless mode (running with no window or interface, in the background; officially called the Agent SDK CLI), it's a fully autonomous robot surgeon: gets the assignment, does it, reports back, shuts down. No dialogue, no "press Enter," no screen. This is how Claude works in CI/CD (Continuous Integration/Delivery) pipelines, GitHub Actions (GitHub's automation system) and cron jobs (tasks run on a schedule): with no human around, 24/7.
Terms in this lesson: headless (running with no window or interface, in the background), CI/CD (continuous integration and delivery), GitHub Actions (GitHub's automation system), cron (a scheduler for running tasks on a timetable), API (application programming interface), token (a unit of text for AI), permission (the right to perform an action), prompt (a request to the AI), agent (an autonomous worker), workflow (a work process).
A note from Anthropic's documentation: what used to be called "headless mode" is now officially called the Agent SDK CLI. The
-pflag and all the options work the same way.
Key concepts
--print/-p: Claude answers once and exits (Agent SDK CLI mode)- Pipe (stdin): pass data in with
cat file | claude -p "..."(10 MB limit) --bare: a fast start without loading hooks/skills/MCP/CLAUDE.md (recommended for CI; requiresANTHROPIC_API_KEY, since subscription sign-in doesn't work in this mode)--output-format: the output format:text,json,stream-json--json-schema: validated JSON matching a schema you provide--max-turns N: cap the number of iterations to control cost--max-budget-usd: a hard spending cap in dollars--permission-mode: permission control:dontAsk,acceptEdits,auto,bypassPermissions--allowedTools: a whitelist of tools to approve automatically- GitHub Actions: the official action
anthropics/claude-code-action@v1 - Environment variables:
ANTHROPIC_API_KEY,CLAUDE_CODE_OAUTH_TOKEN,CLAUDE_CODE_USE_BEDROCK,CLAUDE_CODE_USE_VERTEX
Theory
The --print flag: leaving interactive mode
By default, claude starts a REPL, an interactive session where you talk with Claude. The --print flag (or -p) changes that:
# Interactive mode (waits for input)
claude
# Headless: request → answer → exit
claude --print "Explain what this function does: def f(x): return x * 2"
# Short form
claude -p "Generate a UUID v4 in Python"What happens: Claude receives the request, performs all the needed actions (reads files, runs code, writes the result), prints the answer to stdout and exits with code 0 (success) or a non-zero code (error).
--bare: a fast start for CI/CD
The --bare flag skips auto-loading hooks, skills, plugins, MCP servers, auto-memory and CLAUDE.md. It's the recommended mode for scripts and CI/CD: the result is the same on any machine, and nothing "foreign" gets loaded.
# A fast run without extra context
claude --bare -p "Summarize this file" --allowedTools "Read"In bare mode, Claude has access to Bash and to reading and editing files. Everything else is passed in explicitly through flags. Important: without --bare, running claude -p loads hooks and MCP servers from the project's .claude/settings.json and .mcp.json without a trust dialog, so when running on someone else's code in CI, it's better to use --bare:
| What you need to load | Which flag |
|---|---|
| System prompt | --append-system-prompt or --append-system-prompt-file |
| Settings | --settings <file-or-json> |
| MCP servers | --mcp-config <file-or-json> |
| Your own subagents | --agents <file-or-json> |
| Plugins | --plugin-dir <path> or --plugin-url <url> |
From Anthropic's documentation:
--bareis recommended for scripts and will become the default mode for-pin future versions. In bare mode, Claude Code doesn't read the subscription sign-in (OAuth) or the system keychain, so you need anANTHROPIC_API_KEYfrom the Claude Console (or cloud keys for Bedrock and similar).
Pipe: stdin as input
Passing data through a pipe is a standard Unix pattern. Claude Code fully supports stdin:
# Summarize a log
cat server.log | claude -p "Find all ERROR-level errors, group them by type, show the top 5"
# Code review of a specific file
cat src/payment.py | claude -p "Find potential security vulnerabilities in this code"
# Analyze a git diff before committing
git diff HEAD | claude -p "Write a commit message for these changes in Conventional Commits format"
# Process a CSV
cat leads.csv | claude -p "From this CSV, select the rows where column 'status' = 'qualified', return a JSON array"Piping makes Claude part of standard Unix pipelines: you can drop it into any shell script.
Limit: stdin is capped at 10 MB. Go over it and Claude Code exits with an error. For large files, write the data to a file and put the path in the prompt instead of piping.
--output-format json: machine-readable output
When Claude Code runs in automation, you need to parse its answer programmatically. The --output-format json flag wraps the output in a JSON structure:
claude -p "Check the syntax of this Python file and return a list of errors" \
--output-format json \
< src/main.pyOutput:
{
"type": "result",
"subtype": "success",
"total_cost_usd": 0.0023,
"duration_ms": 1840,
"result": "Found 2 errors:\n1. Line 14: SyntaxError — missing colon after if\n2. Line 31: IndentationError — unexpected indent"
}In a script, you parse it with jq:
RESULT=$(cat src/main.py | claude -p "Find syntax errors" --output-format json)
ERRORS=$(echo "$RESULT" | jq -r '.result')
COST=$(echo "$RESULT" | jq -r '.total_cost_usd')
echo "Analysis cost: $COST USD"
echo "Result: $ERRORS"Three output formats:
| Format | Description | When to use |
|---|---|---|
text |
Plain text (the default) | A human is reading |
json |
JSON with result, session_id, total_cost_usd |
Parsing in scripts |
stream-json |
NDJSON, one JSON object per line, in real time | Streaming, live monitoring |
--json-schema: validated structured output
When you need an answer with a strictly defined structure, use --json-schema. Claude will return JSON validated against the JSON Schema you specify. The result goes in the structured_output field:
# Extract function names in a strictly typed format
claude -p "Extract the function names from auth.py" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}'Parsing the structured output:
# Get the array of functions
claude -p "Extract the functions from auth.py" \
--output-format json \
--json-schema '...' \
| jq '.structured_output'Streaming with stream-json
For live monitoring, use stream-json with --verbose and --include-partial-messages:
# Stream tokens in real time
claude -p "Write a poem" \
--output-format stream-json \
--verbose \
--include-partial-messages \
| jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'Controlling cost and model
# Specify a particular model (alias)
claude -p "Complex architecture analysis" --model opus
# Specify the full model name (example from the docs; current names on the What's current page)
claude -p "Analysis" --model claude-opus-5-5
# A fallback model if the main one is overloaded (you can give a comma-separated list)
claude -p "Request" --fallback-model sonnet
# Limit iterations (to control cost on agentic tasks)
claude -p "Fix the bugs in src/" --max-turns 3
# A hard spending cap in dollars
claude -p "Refactor the auth module" --max-budget-usd 2.00--max-turns is especially important in CI/CD: if Claude keeps trying to fix a bug forever, it gets expensive. A cap of 3-5 iterations is a reasonable limit for automated tasks. When the limit is reached, Claude exits with an error.
--max-budget-usd is a hard spending ceiling. Once Claude has spent the amount you set, it stops. Works only in print mode.
Permission modes for CI/CD
In CI/CD there's no human to press "Yes." Here are the approaches to permissions (the modes are covered in the lesson Permissions and security). If you don't specify a mode, -p uses the default startup mode, which may turn out to be auto, so set the mode explicitly:
# Approach 1: Whitelist specific tools (recommended)
# Claude can only read and run git operations
claude -p "Check the code" --allowedTools "Read" "Bash(git *)"
# Approach 2: dontAsk: only pre-approved actions, everything else is denied
claude -p "Check the code" --permission-mode dontAsk
# Approach 3: acceptEdits: automatically approve file edits
claude -p "Fix the lint errors" --permission-mode acceptEdits
# Approach 4: auto: a reviewer model decides in place of a human
claude -p "Update the dependencies and run the tests" --permission-mode auto --permission-prompts none
# Approach 5: Bypass: ONLY in isolated containers!
claude -p "Fix everything" --dangerously-skip-permissionsThe rule for CI/CD: use the minimum permissions needed. --allowedTools with a whitelist of specific commands is better than --dangerously-skip-permissions.
Wildcards in allowedTools: Bash(git diff *) allows any command that starts with git diff. The space before * matters: without it, Bash(git diff*) would also allow git diff-index.
CI/CD: GitHub Actions
Anthropic's official GitHub Action
Anthropic has an official GitHub Action: anthropics/claude-code-action@v1. You can install it with one command right from Claude Code:
# In an interactive Claude Code session
/install-github-appThe command requires the GitHub CLI to be installed and signed in (gh auth login), admin rights on the repository, and a repository on github.com. Or set it up by hand: install the GitHub App (github.com/apps/claude) and add to the repository secrets either ANTHROPIC_API_KEY (a key from the Claude Console) or CLAUDE_CODE_OAUTH_TOKEN (a Pro, Max, Team or Enterprise subscription token, generated with the claude setup-token command).
A basic workflow that responds to @claude in PR/issue comments:
# .github/workflows/claude.yml
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
claude:
if: contains(github.event.comment.body, '@claude')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Automatically responds to @claude in commentsAutomatic code review on every PR:
# .github/workflows/claude-review.yml
name: Code Review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: "Analyze this PR for code quality, bugs and security. Leave your findings as review comments."
claude_args: "--max-turns 5 --model sonnet"
# To post findings directly in the PR, the action needs an inline-comment tool
# (see the review workflow example in the documentation). For automatic review without your own workflow,
# there's also a ready-made Code Review feature: code.claude.com/docs/en/code-reviewclaude-code-action@v1 parameters:
| Parameter | Description | Required |
|---|---|---|
anthropic_api_key |
Anthropic API key | Yes (for direct API), unless you use claude_code_oauth_token |
claude_code_oauth_token |
Subscription token (from claude setup-token) |
No |
prompt |
Instructions for Claude | No (without it, it responds to @claude) |
claude_args |
Any Claude Code CLI flags | No |
github_token |
GitHub token for the API | No (by default the action runs as the Claude GitHub App) |
trigger_phrase |
Trigger phrase (default @claude) |
No |
plugin_marketplaces, plugins |
Install plugins and run their skills | No |
use_bedrock |
Use Amazon Bedrock | No |
use_vertex |
Use Google Cloud Agent Platform (formerly Vertex AI) | No |
use_foundry |
Use Microsoft Foundry | No |
The manual approach: the Claude CLI in GitHub Actions
If you need full control, you can use claude -p directly:
# .github/workflows/claude-review.yml
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
jobs:
code-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Claude Code
# The npm route works (needs Node.js 22+); the main method now: curl -fsSL https://claude.ai/install.sh | bash
run: npm install -g @anthropic-ai/claude-code
- name: Run Claude Code Review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
# Get the diff of only the changed files
git diff origin/main...HEAD -- '*.py' '*.ts' '*.js' > changes.diff
# Claude analyzes the changes (--bare for a clean CI run)
REVIEW=$(cat changes.diff | claude --bare -p "
You are a senior code reviewer. Analyze this diff.
Look for: bugs, security problems, SOLID violations.
If everything looks good, write 'LGTM'. Mark critical problems with the word CRITICAL.
" --output-format json --max-turns 3 | jq -r '.result')
echo "## Claude Code Review" >> $GITHUB_STEP_SUMMARY
echo "$REVIEW" >> $GITHUB_STEP_SUMMARY
# Check in the same step: the REVIEW variable doesn't carry over between steps
if echo "$REVIEW" | grep -q "CRITICAL"; then
echo "Critical issues found — blocking merge"
exit 1
fiA pre-commit hook with Claude
An automatic code check before every commit:
#!/bin/bash
# .git/hooks/pre-commit
# Get the list of changed Python files
CHANGED_PY=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
if [ -z "$CHANGED_PY" ]; then
exit 0 # No Python files, skip
fi
echo "Claude Code is checking the changes..."
for FILE in $CHANGED_PY; do
RESULT=$(cat "$FILE" | claude -p "
Check this Python file for:
1. Syntax errors
2. Hardcoded secrets (passwords, API keys)
3. SQL injections
If you find a problem, answer 'BLOCK: <description>'.
If everything is clean, answer 'OK'.
" --bare --max-turns 1 --output-format json | jq -r '.result')
if echo "$RESULT" | grep -q "^BLOCK:"; then
echo "Problem in $FILE:"
echo "$RESULT"
exit 1 # Block the commit
fi
done
echo "All checks passed."
exit 0Installing the hook:
chmod +x .git/hooks/pre-commitGenerating a CHANGELOG automatically
#!/bin/bash
# scripts/generate-changelog.sh
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "HEAD~50")
COMMITS=$(git log ${LAST_TAG}..HEAD --oneline)
if [ -z "$COMMITS" ]; then
echo "No new commits"
exit 0
fi
echo "Generating the CHANGELOG with Claude..."
CHANGELOG=$(echo "$COMMITS" | claude -p "
Here's a list of git commits. Generate a CHANGELOG in Keep a Changelog format.
Group them by category: Added, Changed, Fixed, Removed.
Use short, clear descriptions in English.
Start right away with ## [Unreleased]; don't add any intro text.
")
# Prepend it to CHANGELOG.md
echo "$CHANGELOG" | cat - CHANGELOG.md > /tmp/changelog_new
mv /tmp/changelog_new CHANGELOG.md
echo "CHANGELOG.md updated"Authentication and environment variables
To work without a UI, Claude needs an API key. In CI/CD, environment variables are used:
| Variable | Description |
|---|---|
ANTHROPIC_API_KEY |
Anthropic API key (the main method) |
CLAUDE_CODE_USE_BEDROCK=1 |
Use Amazon Bedrock instead of the Anthropic API |
CLAUDE_CODE_USE_VERTEX=1 |
Use Google Cloud (Vertex AI; the documentation now calls it Google Cloud's Agent Platform) |
CLAUDE_CODE_OAUTH_TOKEN |
Subscription token for CI (generated with claude setup-token) |
ANTHROPIC_MODEL |
The default model (overridden by --model) |
Generating a long-lived token for CI:
# Creates an OAuth token and prints it to the terminal (doesn't save it)
# Requires a Claude subscription
claude setup-tokenYou can use the token instead of ANTHROPIC_API_KEY in CI/CD pipelines (through CLAUDE_CODE_OAUTH_TOKEN or the action's claude_code_oauth_token parameter). The exception: the subscription token doesn't work together with --bare; there you need an API key. For a secret shared across a whole organization, the documentation recommends an API key rather than a token: the token is tied to the subscription of the person who created it.
Continuing sessions in scripts
You can build chains of calls that carry on the previous context:
# First request: analysis
claude -p "Analyze the performance of this project"
# Continue the last conversation
claude -p "Now focus on the SQL queries" --continue
# Or use a session ID to be safe
SESSION=$(claude -p "Start the review" --output-format json | jq -r '.session_id')
claude -p "Continue the review" --resume "$SESSION"Cost/speed patterns in headless mode
| Task | Model | max-turns | Rough cost per run |
|---|---|---|---|
| Syntax check | haiku | 1 | minimal |
| Code review of a diff | sonnet | 1 | low |
| Changelog generation | sonnet | 1 | low |
| Automatic bug fixing | sonnet | 5 | medium |
| Complex refactoring | opus | 10 | above medium |
For the exact cost of a run, look at the total_cost_usd field in the response (it's a client-side estimate and may differ from your actual bill). Token prices: current prices and versions: What's current.
Rules for controlling cost in CI/CD:
--max-turns 1for analysis (read only + output)--max-turns 3-5for tasks that change files--max-budget-usd 5.00: a hard spending ceiling per run--bare: don't load extra context (saves time and tokens)--model sonnetfor routine work,--model opusonly for the complex stuff
Practice
Assignment: a pre-commit hook that looks for secrets
- Create a test git repository:
git init test-repo && cd test-repo - Create a
.git/hooks/pre-commitfile with the content from the example above (a simplified version that only looks for secrets) - Make the hook executable:
chmod +x .git/hooks/pre-commit - Create a
config.pyfile with this text:python API_KEY = "sk-1234567890abcdef" # test key DATABASE_URL = "postgresql://user:password@localhost/db" - Try to commit:
git add config.py && git commit -m "test". The hook should block it - Remove the secrets (use environment variables) and commit again. It should go through
- Bonus: add commit message generation to the hook with
git diff --cached | claude -p "Write a commit message"
Goal: understand how Claude Code works without a UI and how to build it into automated pipelines.
Tools and resources
claude -p "...": a headless request (Agent SDK CLI mode)--bare: a fast start with no context (recommended for CI)--output-format json: structured output (result,total_cost_usd,session_id)--json-schema: validated structured output against a JSON Schema--output-format stream-json: NDJSON streaming--max-turns N: an iteration cap--max-budget-usd N: a hard spending cap--allowedTools: a tool whitelist with wildcard support--permission-mode:dontAsk,acceptEdits,bypassPermissions--continue/--resume: continuing sessions in scriptsclaude setup-token: generates a long-lived OAuth token for CIanthropics/claude-code-action@v1: the official GitHub Action/install-github-app: quick GitHub App setup from Claude Codejq:brew install jq, for parsing JSON in bash scripts- Documentation: https://code.claude.com/docs/en/headless
Key takeaways
claude --bare -p "request"= the recommended format for CI/CD.--barefor a clean start,-pfor headless. No interaction, the same result on any machine.
Piping (
cat file | claude -p "...") makes Claude part of a Unix pipeline. The stdin limit is 10 MB. For larger data, put the file path in the prompt.
Three levels of safety in CI:
--allowedTools(a whitelist of specific commands) is better than--permission-mode dontAsk, which is better than--dangerously-skip-permissions. Use the minimum permissions needed.
The official GitHub Action (
anthropics/claude-code-action@v1) is simpler than a manual setup. It responds to@claudein comments, supports skills, and takes any CLI flags throughclaude_args.
Cost control:
--max-turns 3+--max-budget-usd 5.00+--model sonnet= sensible limits for automated tasks.
What's next
The mark stays in this browser only and is never sent anywhere. My progress