Library · Automation: browser, screen and schedules

Headless Mode and CI/CD: Claude without a UI

Engineer65 minUpdated: October 2026
43 of 105 in the library

Module: 9. Advanced features | Time: ~25 min theory + 40 min practice


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 -p flag 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; requires ANTHROPIC_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

🎨 Picture this: --print is like a coffee vending machine instead of a barista. Press the button, get your cup, the machine goes idle. No "how's your day," no follow-up questions. One request, one answer, done.

By default, claude starts a REPL, an interactive session where you talk with Claude. The --print flag (or -p) changes that:

bash
# 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.

bash
# 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>

🎨 Picture this: --bare is like starting a program in "safe mode." Nothing extra loads, only what you explicitly specified. For CI/CD this matters: you don't want someone's personal hooks on the build server.

From Anthropic's documentation: --bare is recommended for scripts and will become the default mode for -p in future versions. In bare mode, Claude Code doesn't read the subscription sign-in (OAuth) or the system keychain, so you need an ANTHROPIC_API_KEY from the Claude Console (or cloud keys for Bedrock and similar).


Pipe: stdin as input

🎨 Picture this: Piping into Claude is like a factory conveyor. A part comes off one machine (cat), rides the belt (|) and arrives at the next machine (claude). No need to carry it over by hand; it all happens automatically.

Passing data through a pipe is a standard Unix pattern. Claude Code fully supports stdin:

bash
# 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

🎨 Picture this: JSON output is like an official form instead of a handwritten letter. A machine reads the form automatically: the "result" field goes here, the "cost" field goes there. A letter would have to be picked apart by hand.

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:

bash
claude -p "Check the syntax of this Python file and return a list of errors" \
       --output-format json \
       < src/main.py

Output:

json
{
  "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:

bash
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:

bash
# 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:

bash
# Get the array of functions
claude -p "Extract the functions from auth.py" \
  --output-format json \
  --json-schema '...' \
  | jq '.structured_output'

🎨 Picture this: --output-format json is like asking for a report in a standard format. --json-schema is like handing over a specific blank form: "fill in EXACTLY these fields." The machine gets exactly what it expects.


Streaming with stream-json

For live monitoring, use stream-json with --verbose and --include-partial-messages:

bash
# 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

bash
# 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:

bash
# 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-permissions

The 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:

bash
# In an interactive Claude Code session
/install-github-app

The 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:

yaml
# .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 comments

Automatic code review on every PR:

yaml
# .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-review

claude-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:

yaml
# .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
          fi

A pre-commit hook with Claude

🎨 Picture this: A pre-commit hook is like an inspection at the factory exit. You've made a part, and before it goes to the warehouse (the commit), a guard checks it for prohibited materials (secrets, SQL injections). If he finds any, he sends it back.

An automatic code check before every commit:

bash
#!/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 0

Installing the hook:

bash
chmod +x .git/hooks/pre-commit

Generating a CHANGELOG automatically

bash
#!/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:

bash
# Creates an OAuth token and prints it to the terminal (doesn't save it)
# Requires a Claude subscription
claude setup-token

You 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:

bash
# 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"

🎨 Picture this: --max-turns 3 in CI/CD is like a kitchen timer. The agent tries to fix a bug, doesn't manage, tries again, and again, and then stop. Without the timer it could keep trying forever and burn through the budget.

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 1 for analysis (read only + output)
  • --max-turns 3-5 for 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 sonnet for routine work, --model opus only for the complex stuff

🎨 Picture this: A robot surgeon. In regular mode, Claude is a surgeon with an assistant: it proposes an incision, you approve. In headless mode, it's a fully autonomous robot: gets the instructions, performs the operation, puts the report on the desk. No dialogue. That's exactly what you need when GitHub Actions is checking your pull request at 3 a.m.


Practice

Assignment: a pre-commit hook that looks for secrets

  1. Create a test git repository: git init test-repo && cd test-repo
  2. Create a .git/hooks/pre-commit file with the content from the example above (a simplified version that only looks for secrets)
  3. Make the hook executable: chmod +x .git/hooks/pre-commit
  4. Create a config.py file with this text:
    python
    API_KEY = "sk-1234567890abcdef"  # test key
    DATABASE_URL = "postgresql://user:password@localhost/db"
  5. Try to commit: git add config.py && git commit -m "test". The hook should block it
  6. Remove the secrets (use environment variables) and commit again. It should go through
  7. 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 scripts
  • claude setup-token: generates a long-lived OAuth token for CI
  • anthropics/claude-code-action@v1: the official GitHub Action
  • /install-github-app: quick GitHub App setup from Claude Code
  • jq: 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. --bare for a clean start, -p for 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 @claude in comments, supports skills, and takes any CLI flags through claude_args.

Cost control: --max-turns 3 + --max-budget-usd 5.00 + --model sonnet = sensible limits for automated tasks.


What's next

→ Extended Thinking: thinking things through

The mark stays in this browser only and is never sent anywhere. My progress