Skip to main content

Headless Automation Mode

Headless mode runs Claude Code directly from a script or pipeline with no interactive conversation. Use it for CI/CD, batch jobs, code generation automation, and similar tasks.

Official name change

Anthropic's official documentation has rebranded this feature as the Agent SDK CLI. The -p flag and all CLI options work exactly the same.

The --print flagโ€‹

With the --print (-p) flag, Claude Code handles a single request, prints the result to stdout, and exits.

# Basic usage
claude --print "Analyze the complexity of this function" < src/complex.ts

# Pipe file contents in
cat error.log | claude --print "Analyze the cause of this error"

# Save the result to a file
claude --print "Write the API specification" > docs/api.md

Auto-approving permissionsโ€‹

In a headless environment there is no one to approve prompts, so permissions must be configured up front:

# Auto-approve all permissions (use only in an isolated CI environment)
claude --dangerously-skip-permissions --print "Fix the code"

Or auto-approve only specific tools via settings.json:

{
"permissions": {
"allow": ["Read(*)", "Write(*)", "Edit(*)", "Bash(npm test)"],
"deny": ["Bash(rm *)"]
}
}

Practical automation scriptsโ€‹

Batch file processingโ€‹

Process many files automatically:

#!/bin/bash
# Add JSDoc to every TypeScript file

for file in src/**/*.ts; do
echo "Processing: $file"
claude --print "Add JSDoc comments to the public functions and classes in this file.
Do not change any existing code logic โ€” add comments only." < "$file" > "${file}.tmp"

# Verify the result before replacing
if [ -s "${file}.tmp" ]; then
mv "${file}.tmp" "$file"
else
rm "${file}.tmp"
echo "Warning: failed to process $file"
fi
done

echo "Done!"

Generating a code review report automaticallyโ€‹

#!/bin/bash
# Generate a PR review report

BRANCH=${1:-HEAD}
BASE=${2:-main}

DIFF=$(git diff $BASE...$BRANCH)

if [ -z "$DIFF" ]; then
echo "No changes"
exit 0
fi

echo "$DIFF" | claude --print "Review this set of code changes.
Write a markdown report in the following format:
## Summary
## Key Changes
## Potential Issues
## Suggested Improvements
## Verdict (Approve / Request changes)" > review-report.md

echo "Review report generated: review-report.md"

Automated migrationโ€‹

#!/bin/bash
# API v1 โ†’ v2 migration

FILES=$(grep -rl "api/v1" src/)

for file in $FILES; do
echo "Migrating: $file"
cat "$file" | claude --print "Update the 'api/v1' endpoints in this file to 'api/v2'.
Change only the endpoint paths and keep the logic as is.
Output only the full modified file contents (no explanation)." > "$file.migrated"

mv "$file.migrated" "$file"
done

Output format (--output-format)โ€‹

The --output-format flag controls the response format:

FormatDescriptionUse case
textPlain text (default)Human reading
jsonStructured JSON (includes session ID and metadata)Parsing in scripts
stream-jsonNewline-delimited JSON streamingReal-time processing
# Get the result as JSON
claude -p "Summarize the project" --output-format json

# Extract just the result text with jq
claude -p "Summarize the project" --output-format json | jq -r '.result'

Structured output with a JSON schemaโ€‹

You can force a specific output shape with --json-schema:

# Extract function names as an array
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"]}'

# Structured output is returned in the .structured_output field
claude -p "Analyze the code" --output-format json --json-schema '...' | jq '.structured_output'

Real-time streamingโ€‹

# Receive tokens in real time as they are generated
claude -p "Explain recursion" --output-format stream-json --verbose

# Print only the text 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'

Continuing a conversation (--continue, --resume)โ€‹

Multi-turn conversations work in headless mode too:

# First request
claude -p "Review the performance problems in this codebase"

# Continue the most recent conversation
claude -p "Focus on the database queries" --continue
claude -p "Summarize the issues you found" --continue

To manage several conversations at once, use session IDs:

# Capture the session ID
session_id=$(claude -p "Start the review" --output-format json | jq -r '.session_id')

# Resume a specific session
claude -p "Continue the review" --resume "$session_id"

Auto-approving tools (--allowedTools)โ€‹

Auto-approve specific tools without prompting:

# Allow file reads/edits and Bash
claude -p "Run the tests and fix the failures" \
--allowedTools "Bash,Read,Edit"

# Create a commit (allow git commands only)
claude -p "Create an appropriate commit from the staged changes" \
--allowedTools "Bash(git diff *),Bash(git log *),Bash(git status *),Bash(git commit *)"
Pattern matching with a trailing *

Bash(git diff *) allows every command starting with git diff. The space before the * matters โ€” without it, other commands like git diff-index would match too.

Customizing the system promptโ€‹

# Append to the existing system prompt
gh pr diff "$1" | claude -p \
--append-system-prompt "Review for vulnerabilities from a security engineer's perspective." \
--output-format json

# Replace the system prompt entirely
claude -p "Analyze the code" \
--system-prompt "You are a performance optimization expert."

Using environment variablesโ€‹

Inject context into a headless script:

#!/bin/bash
# Inject environment-specific configuration

ENVIRONMENT=${APP_ENV:-development}
DB_TYPE=${DB_TYPE:-postgresql}

claude --print "Adapt this migration script for a $DB_TYPE database
in the $ENVIRONMENT environment." < migration.sql

Error handling and retriesโ€‹

#!/bin/bash
MAX_RETRIES=3
RETRY_COUNT=0

while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
RESULT=$(claude --print "Analyze the code" < src/main.ts 2>&1)
EXIT_CODE=$?

if [ $EXIT_CODE -eq 0 ]; then
echo "$RESULT"
break
else
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "Failed ($RETRY_COUNT/$MAX_RETRIES): $RESULT"
sleep 5
fi
done

if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
echo "Maximum retries exceeded"
exit 1
fi

Make / task runner integrationโ€‹

Add Claude tasks to a Makefile:

# Makefile

review:
@echo "Running code review..."
@git diff main...HEAD | claude --print "Review this code" > review.md
@echo "Review complete: review.md"

docs:
@find src -name "*.ts" -exec sh -c \
'claude --print "Generate API documentation for this file" < {} > docs/{}.md' \;

migrate:
@claude --print "Generate a migration for these schema changes" \
< schema.diff > migrations/$$(date +%Y%m%d_%H%M%S)_auto.sql
Headless mode caveats
  • Use --dangerously-skip-permissions only in an isolated environment (Docker, a CI sandbox)
  • Code generated headlessly must always be reviewed by a human before deployment
  • Cost monitoring is essential โ€” large batch runs can produce unexpected costs