Most developers use Claude Code interactively. But the same tool that handles conversations also runs completely headlessly, producing structured output suitable for pipelines, scripts, and CI systems.
The -p flag (print mode) is how Claude Code integrates with everything outside its own UI. This guide covers every aspect of headless Claude Code: the output formats, structured output with JSON schemas, streaming patterns, the --bare mode for fast automation, and real-world patterns for CI/CD integration.
The -p Flag: Headless Execution
# Basic headless query
claude -p "How many exported functions are in src/api/?"
# With explicit stdin
echo "$(cat package.json)" | claude -p "What dependencies are outdated?"
# From file content
claude -p "Summarize this file: $(cat ARCHITECTURE.md)"
When run with -p, Claude Code:
- Executes without opening the interactive UI
- Reads a single prompt, processes it, and exits
- Outputs the response to stdout
- Writes errors to stderr
- Returns exit code 0 on success, non-zero on failure
The prompt can access all tools (file reading, bash execution, etc.) exactly as in interactive mode, unless restricted with --allowedTools or --disallowedTools.
Output Formats
text (default)
claude -p "List the three most complex functions in this codebase"
# Output: plain markdown text
Readable, but hard to parse programmatically.
json
claude -p "List the three most complex functions" --output-format json
Returns a JSON object with:
{
"result": "1. processPaymentTransaction...\n2. handleOAuthCallback...",
"session_id": "7f3a9c2b-4e81-4c3d-a9f2-3b8c1d7e0a65",
"total_cost_usd": 0.0043,
"cost_breakdown": [
{
"model": "claude-sonnet-4-6",
"input_tokens": 4821,
"output_tokens": 312,
"cost_usd": 0.0043
}
]
}
Useful when you need:
- The session ID for later resumption
- Cost tracking per query
- The result as part of a larger JSON pipeline
stream-json
claude -p "Explain recursion" --output-format stream-json --verbose
Emits newline-delimited JSON events as they arrive. Each line is a complete JSON object.
The --verbose flag includes tool calls and system events. Without it, only the final result is streamed.
Add --include-partial-messages to receive partial text tokens as they arrive (useful for progressive display):
claude -p "Write a detailed analysis" \
--output-format stream-json \
--verbose \
--include-partial-messages
stream-json Event Types
system/init
Emitted at session start. Contains session metadata:
{
"type": "system/init",
"session_id": "7f3a9c2b-...",
"model": "claude-sonnet-4-6",
"tools": ["Read", "Write", "Bash", "Edit"],
"mcp_servers": ["filesystem", "github"],
"plugins": [],
"timestamp": "2026-06-17T08:14:22Z"
}
Use this to capture the session ID at stream start for later resumption.
stream_event
The primary event type. Contains incremental text output:
{
"type": "stream_event",
"event": {
"delta": {
"type": "text_delta",
"text": "The three "
}
}
}
For tool calls, delta.type is "input_json_delta" or "tool_use".
system/api_retry
Emitted before a retry on API errors:
{
"type": "system/api_retry",
"attempt": 2,
"retry_delay_ms": 2000,
"error": "overloaded_error"
}
Final result event
At completion:
{
"type": "result",
"result": "The complete final text response",
"session_id": "7f3a9c2b-...",
"total_cost_usd": 0.0043
}
Parsing stream-json with jq
Extract text tokens as they arrive:
claude -p "Write a poem about deployment" \
--output-format stream-json \
--verbose \
--include-partial-messages | \
jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'
Capture session ID at start, text at end:
OUTPUT=$(claude -p "Analyze the auth module" --output-format stream-json --verbose)
SESSION_ID=$(echo "$OUTPUT" | jq -r 'select(.type == "system/init") | .session_id' | head -1)
RESULT=$(echo "$OUTPUT" | jq -r 'select(.type == "result") | .result' | head -1)
COST=$(echo "$OUTPUT" | jq -r 'select(.type == "result") | .total_cost_usd' | head -1)
echo "Session: $SESSION_ID"
echo "Cost: \$$COST"
echo "Result: $RESULT"
Structured Output with --json-schema
When you need a guaranteed JSON structure (not just markdown that happens to look like JSON):
claude -p "Extract all API endpoints from this codebase" \
--output-format json \
--json-schema '{
"type": "object",
"properties": {
"endpoints": {
"type": "array",
"items": {
"type": "object",
"properties": {
"method": {"type": "string"},
"path": {"type": "string"},
"handler": {"type": "string"},
"auth_required": {"type": "boolean"}
},
"required": ["method", "path", "handler"]
}
}
}
}'
The result field in the JSON output will be a string containing valid JSON conforming to the schema. Parse it with:
RESULT=$(claude -p "Extract API endpoints" --output-format json --json-schema "$SCHEMA")
echo "$RESULT" | jq '.result | fromjson | .endpoints[] | "\(.method) \(.path)"'
Practical schemas for common automation tasks:
# Bug detection
BUGS_SCHEMA='{
"type": "object",
"properties": {
"bugs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"file": {"type": "string"},
"line": {"type": "integer"},
"severity": {"type": "string", "enum": ["critical", "high", "medium", "low"]},
"description": {"type": "string"},
"suggested_fix": {"type": "string"}
},
"required": ["file", "severity", "description"]
}
},
"summary": {"type": "string"}
}
}'
# Test coverage gaps
COVERAGE_SCHEMA='{
"type": "object",
"properties": {
"uncovered_functions": {"type": "array", "items": {"type": "string"}},
"risk_areas": {"type": "array", "items": {"type": "string"}},
"recommended_tests": {"type": "array", "items": {"type": "string"}}
}
}'
--bare Mode for Fast Automation
The --bare flag skips startup overhead that isn’t needed in automation:
claude --bare -p "Summarize CHANGELOG.md" --allowedTools "Read"
--bare skips:
- CLAUDE.md loading
- Hooks (PreToolUse, PostToolUse, etc.)
- Plugin loading
- MCP server connections
- Auto memory
This reduces startup time significantly when you don’t need project-specific configuration. For automation scripts that run thousands of times, the cumulative savings add up.
Note: --bare is expected to become the default for -p in a future release.
Tool Control in Headless Mode
Restrict which tools Claude can use to limit scope and prevent unintended side effects:
# Read-only analysis (no writes, no bash)
claude -p "Analyze the security model" --allowedTools "Read"
# Specific tools only
claude -p "Check for syntax errors" --allowedTools "Read,Bash"
# Disable specific tools while keeping others
claude -p "Refactor this function" --disallowedTools "Bash"
For complete automation (no tool confirmation prompts):
claude -p "Fix all TypeScript errors" \
--dangerously-skip-permissions \
--allowedTools "Read,Edit,Write,Bash"
Use --dangerously-skip-permissions only in sandboxed CI environments where you’ve validated what Claude will do.
Practical CI/CD Patterns
Pattern 1: Pre-Commit Analysis
#!/bin/bash
# .git/hooks/pre-commit
CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.ts$' | head -20)
if [ -z "$CHANGED_FILES" ]; then
exit 0
fi
FILES_CONTENT=$(echo "$CHANGED_FILES" | xargs -I {} sh -c 'echo "=== {} ===" && cat {}')
RESULT=$(claude --bare -p "Review these changed TypeScript files for critical bugs only. Be concise. Files:\n$FILES_CONTENT" \
--output-format json \
--allowedTools "" \
--json-schema '{
"type": "object",
"properties": {
"critical_bugs": {"type": "array", "items": {"type": "string"}},
"blocking": {"type": "boolean"}
}
}')
BLOCKING=$(echo "$RESULT" | jq '.result | fromjson | .blocking')
BUGS=$(echo "$RESULT" | jq -r '.result | fromjson | .critical_bugs[]' 2>/dev/null)
if [ "$BLOCKING" = "true" ]; then
echo "❌ Claude Code found critical bugs:"
echo "$BUGS"
exit 1
fi
exit 0
Pattern 2: PR Review Comment
#!/bin/bash
# Called from GitHub Actions on PR open
PR_DIFF=$(git diff origin/main...HEAD)
PR_NUMBER=$1
REVIEW=$(claude --bare -p "Review this PR diff and provide specific, actionable feedback. Focus on bugs, security issues, and performance problems. Skip style comments.\n\nDiff:\n$PR_DIFF" \
--output-format json \
--allowedTools "Read" \
--json-schema '{
"type": "object",
"properties": {
"summary": {"type": "string"},
"issues": {
"type": "array",
"items": {
"type": "object",
"properties": {
"severity": {"type": "string", "enum": ["critical", "major", "minor"]},
"file": {"type": "string"},
"description": {"type": "string"}
}
}
},
"recommendation": {"type": "string", "enum": ["approve", "request_changes", "comment"]}
}
}')
COMMENT=$(echo "$REVIEW" | jq -r '.result | fromjson | "## Claude Code Review\n\n**Recommendation:** \(.recommendation)\n\n**Summary:** \(.summary)\n\n### Issues\n\(.issues[] | "- **\(.severity)** (`\(.file)`): \(.description)")"')
gh pr comment "$PR_NUMBER" --body "$COMMENT"
Pattern 3: Multi-Step Analysis Pipeline
#!/bin/bash
# Step 1: Find candidates
STEP1=$(claude -p "Find functions that look like they might have memory leaks" \
--output-format json \
--json-schema '{"type":"object","properties":{"suspects":{"type":"array","items":{"type":"string"}}}}')
SESSION_ID=$(echo "$STEP1" | jq -r '.session_id')
SUSPECTS=$(echo "$STEP1" | jq -r '.result | fromjson | .suspects[]')
# Step 2: Deep analysis of each suspect (resuming context from step 1)
for func in $SUSPECTS; do
claude -p "Analyze this function in detail for memory leaks: $func" \
--session-id "$SESSION_ID" \
--output-format json | \
jq -r '.result' >> leak_report.md
done
echo "Report written to leak_report.md"
Pattern 4: Periodic Codebase Health Check
#!/bin/bash
# Run daily via cron
HEALTH=$(claude --bare \
-p "Run a health check on this codebase. Check: 1) Files >1000 lines that should be split, 2) Circular dependencies, 3) Hardcoded credentials patterns, 4) TODO comments older than 30 days" \
--output-format json \
--allowedTools "Read,Bash" \
--json-schema '{
"type": "object",
"properties": {
"score": {"type": "integer", "minimum": 0, "maximum": 100},
"issues": {"type": "array", "items": {"type": "string"}},
"urgent": {"type": "array", "items": {"type": "string"}}
}
}')
SCORE=$(echo "$HEALTH" | jq '.result | fromjson | .score')
URGENT=$(echo "$HEALTH" | jq -r '.result | fromjson | .urgent[]' 2>/dev/null)
if [ "$SCORE" -lt 70 ]; then
echo "⚠️ Codebase health: $SCORE/100"
echo "Urgent issues:"
echo "$URGENT"
# Send alert to Slack, PagerDuty, etc.
fi
Passing Context Efficiently
For large codebases, be explicit about which files Claude should read:
# Pass file list in prompt
RELEVANT_FILES=$(grep -rl "authentication" src/ | head -10 | tr '\n' ' ')
claude -p "Analyze authentication flow. Relevant files: $RELEVANT_FILES" \
--allowedTools "Read"
# Or pipe structured context
{
echo "Analyze these specific functions:"
grep -n "function process\|async function handle" src/api/*.ts
} | claude -p "$(cat)" --allowedTools "Read"
For multi-file analysis, explicitly pre-read files in the prompt rather than letting Claude discover them:
CONTEXT=$(for f in src/auth.ts src/session.ts src/middleware.ts; do
echo "=== $f ==="
cat "$f"
done)
claude -p "Given this auth stack:\n$CONTEXT\n\nFind the session fixation vulnerability" \
--bare \
--allowedTools ""
Cost and Rate Limit Considerations
The --output-format json response includes cost data. Track it:
RESULT=$(claude -p "Analyze the full codebase" --output-format json)
COST=$(echo "$RESULT" | jq '.total_cost_usd')
echo "Query cost: \$$COST"
For high-volume automation, use --model to select a cheaper model when task complexity is low:
# Use Haiku for simple classification tasks
claude -p "Is this a bug report or a feature request: $ISSUE_BODY" \
--model claude-haiku-4-5-20251001 \
--output-format json \
--json-schema '{"type":"object","properties":{"type":{"type":"string","enum":["bug","feature","other"]}}}'
# Reserve Sonnet/Opus for complex analysis
claude -p "Perform a security audit of the auth module" \
--model claude-sonnet-4-6 \
--allowedTools "Read"
Summary
| Goal | Pattern |
|---|---|
| Simple headless query | claude -p "query" |
| Get structured result | claude -p "..." --output-format json |
| Enforce JSON schema | Add --json-schema '...' |
| Stream tokens in real time | --output-format stream-json --include-partial-messages |
| Fast automation (no config overhead) | Add --bare |
| Read-only analysis | Add --allowedTools "Read" |
| No confirmation prompts (CI) | Add --dangerously-skip-permissions |
| Multi-step pipeline with context | Capture session_id, pass via --session-id |
| Cost tracking | --output-format json → .total_cost_usd |
Print mode is where Claude Code scales from a developer tool to a platform. Once you understand the output formats and tool controls, integrating Claude into any pipeline is straightforward.