Claude Code automation CI/CD shell headless stream-json

Claude Code Print Mode and stream-json: Automate Everything with -p

Prompt Shelf ·

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

GoalPattern
Simple headless queryclaude -p "query"
Get structured resultclaude -p "..." --output-format json
Enforce JSON schemaAdd --json-schema '...'
Stream tokens in real time--output-format stream-json --include-partial-messages
Fast automation (no config overhead)Add --bare
Read-only analysisAdd --allowedTools "Read"
No confirmation prompts (CI)Add --dangerously-skip-permissions
Multi-step pipeline with contextCapture 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.


Related Articles

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules