Языки: EN RU

Headless Mode and CLI Scripting

Some tasks don't need an interactive dialogue. Code review in CI, automatic explanation of build errors, nightly dependency audits — these are all just scripts that should run and return a result. The -p flag (short for --print) puts Claude Code into exactly this mode: prompt → agent cycle → output result → exit.

This article is about how to embed Claude Code into scripts, pipelines, and CI/CD so that it behaves like a proper Unix tool: reads stdin, writes to stdout, returns structured output, and requires nothing extra.


-p: the core headless flag

The simplest case — ask a question about the codebase and get an answer:

claude -p "Что делает модуль auth?"

The agent reads files in the current directory, responds, and exits. Unlike interactive mode — no REPL, no permission prompts on stdin. This is the key point: in -p mode Claude Code still runs a full agent cycle (reads files, executes commands — if permitted), but does so without pausing for confirmation.

# Explain a specific file
claude -p "Объясни логику в src/parsers/csv.ts, особенно обработку quoted fields"

# Quick code review of changes
git diff HEAD~1 | claude -p "Сделай краткий code review этого diff"

# Continue a previous session in headless mode
claude -c -p "Теперь сосредоточься на database queries"
Проверь себя
Suppose you run `claude -p 'Explain the project'` without the `--allowedTools` flag — will the agent be able to read files in the current directory?

Pipes and composition with Unix utilities

Headless mode reads stdin — this opens up standard Unix patterns.

# Explain a build error
cat build-error.txt | claude -p 'Объясни корневую причину этой ошибки сборки'

# Watch a log and explain anomalies in real time
tail -f app.log | claude -p 'Предупреди, если увидишь паттерны ошибок или аномалии'

# Pass a PR diff directly from the gh CLI
gh pr diff 123 | claude -p 'Проверь на потенциальные проблемы безопасности'

One practical limitation: stdin is capped at 10 MB. If the content is larger, Claude Code exits with an error. For large files it's better to specify the path in the prompt rather than piping the content.

Composition works in both directions: Claude Code's output can also be piped into other utilities:

# Get a list of files with issues and pass them to grep
claude -p 'Перечисли файлы с потенциальными утечками памяти, по одному пути на строку' | \
  xargs grep -l 'malloc'
flowchart LR A["stdin / file / git diff"] --> B["claude -p 'prompt'" --output-format json --allowedTools ... --bare] B --> C["Agent loop Read / Bash / Edit"] C --> D["stdout: JSON"] D --> E1["jq '.result'"] D --> E2[">> report.md"] D --> E3["| grep ERROR"]
flowchart LR
    A["stdin / file / git diff"] --> B["claude -p 'prompt'"
--output-format json
--allowedTools ...
--bare]
    B --> C["Agent loop
Read / Bash / Edit"]
    C --> D["stdout: JSON"]
    D --> E1["jq '.result'"]
    D --> E2[">> report.md"]
    D --> E3["| grep ERROR"]
Headless mode as a pipeline building block: Claude Code reads from stdin, runs the agent loop, and writes JSON to stdout — just like any Unix utility

Output formats: text, json, stream-json

By default, -p returns plain text — convenient for reading, but inconvenient for parsing in scripts. The --output-format flag changes that.

text — standard text, as in the terminal.

json — a structured object with metadata:

claude -p "Summarize this project" --output-format json

The output looks roughly like this:

{
  "result": "This project is a CSV parser library that...",
  "session_id": "sess_01AbCdEf...",
  "total_cost_usd": 0.0034,
  "cost_breakdown": {
    "input_tokens": 1200,
    "output_tokens": 340
  }
}

The total_cost_usd field lets you track spend at the script level — useful in CI for cost tracking without visiting the dashboard.

Parsing with jq:

# Result text only
claude -p "Summarize this project" --output-format json | jq -r '.result'

# Cost only
claude -p "Review this code" --output-format json | jq '.total_cost_usd'

stream-json — a stream of JSON objects, one per line, generated as output is produced. Each object is an agent cycle event. Use this when you need to display progress or process tokens as they arrive:

claude -p "Write a detailed explanation of this algorithm" \
  --output-format stream-json \
  --verbose \
  --include-partial-messages | \
  jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'

The --verbose and --include-partial-messages flags are required for stream-json to include intermediate events rather than only the final result.

Проверь себя
What is the difference between `--output-format json` and `--output-format stream-json`? When would you choose each?

Structured output with --json-schema

Combining --output-format json with --json-schema produces guaranteed-valid JSON conforming to a given schema. The agent completes the agent cycle and returns a structure matching the schema — in the structured_output field:

claude -p "Extract all public API endpoints from src/api/" \
  --output-format json \
  --json-schema '{
    "type": "object",
    "properties": {
      "endpoints": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "method": {"type": "string"},
            "path": {"type": "string"},
            "description": {"type": "string"}
          }
        }
      }
    }
  }' | jq '.structured_output.endpoints'

This is a powerful pattern for parsing codebases: the agent inspects the files and you get the data as an array or object — no regular expressions, no brittle text parsing.


--bare: fast startup for CI

By default, claude -p loads everything an interactive session does on startup: CLAUDE.md, hooks, skills, MCP servers, auto-memory. In CI this is often unnecessary — and slow.

The --bare flag skips auto-discovery of all of the above:

claude --bare -p "Summarize this file" --allowedTools "Read"

In bare mode only the basic tools are available (Bash, file read and edit). Everything else is wired in explicitly via flags:

claude --bare -p "Review this PR" \
  --mcp-config ./ci-mcp.json \
  --append-system-prompt "You are a security reviewer."

One important practical consequence: bare mode does not read ANTHROPIC_API_KEY from the keychain — authentication must come via an environment variable or --settings. In CI this is typically how it's configured anyway.

According to the documentation, --bare will become the default mode for -p in future versions.


Tool control: --allowedTools

In headless mode there is no one to accept a permission prompt — so you need to explicitly declare what the agent may do without asking:

# Read-only — the agent will not modify any files
claude -p "Find all TODO comments in the codebase" \
  --allowedTools "Read,Bash(grep *),Bash(find *)"

# Read, edit, and specific git commands
claude -p "Look at staged changes and create an appropriate commit" \
  --allowedTools "Bash(git diff *),Bash(git log *),Bash(git status *),Bash(git commit *)"

# acceptEdits mode — writes files without prompting, but not shell commands
claude -p "Apply lint fixes" --permission-mode acceptEdits

The Bash(git diff *) syntax is permission rule syntax: the space before * matters. Bash(git diff *) allows any command starting with git diff, whereas Bash(git diff*) without the space does not.

Проверь себя
Why is `Bash(git diff *)` with a space before the asterisk not the same as `Bash(git diff*)`?

Limits: --max-turns and --max-budget-usd

Two flags that make headless runs predictable in terms of cost and time:

# No more than 5 agent steps
claude -p "Investigate the failing tests" \
  --max-turns 5 \
  --output-format json

# Maximum $2 per run
claude -p "Refactor the auth module" \
  --max-budget-usd 2.00

If the agent reaches the --max-turns limit it exits with a non-zero return code. This is handy in CI: if claude -p ... --max-turns 10; then ... fi.

--max-budget-usd only works with -p and tracks total_cost_usd for the current run. This is a coarse limit, not a billing control — for precise accounting use the total_cost_usd field from the JSON output.


Practical patterns

Typo linter in package.json:

{
  "scripts": {
    "lint:typos": "git diff main | claude -p \"you are a typo linter. for each typo in this diff, report filename:line on one line and the issue on the next. return nothing else.\""
  }
}

Security review a PR before merging:

#!/bin/bash
PR_NUMBER=$1

gh pr diff "$PR_NUMBER" | claude --bare -p \
  --append-system-prompt "You are a security engineer. Focus on injection, auth bypass, and data exposure." \
  --output-format json \
  --max-budget-usd 1.00 \
  "Review this PR diff for security vulnerabilities. List each issue with severity." \
  | jq -r '.result'

Multi-step analysis with session resumption:

# First request — save the session_id
session_id=$(claude -p "Review this codebase for performance issues" \
  --output-format json | jq -r '.session_id')

# Subsequent requests in the same session
claude -p "Now focus only on database queries" \
  --resume "$session_id" \
  --output-format json | jq -r '.result'

claude -p "Generate a prioritized fix list" \
  --resume "$session_id" \
  --output-format json | jq -r '.result'

This is especially useful when each analysis step builds on the context of the previous one — the agent remembers what it already found.


Headless in CI/CD

Headless mode is the foundation for integrating Claude Code into GitHub Actions and other CI systems. A full pipeline is covered in GitHub Actions and Automated Code Review, but the basic principle is simple:

# .github/workflows/claude-review.yml
- name: Claude security review
  run: |
    gh pr diff ${{ github.event.pull_request.number }} | \
      claude --bare -p \
        --append-system-prompt "Security reviewer. Output JSON only." \
        --output-format json \
        --max-budget-usd 2.00 \
        "Find security vulnerabilities" \
      | jq '.result'
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

Note the --bare flag: without it the agent will attempt to load CLAUDE.md and MCP servers from the CI runner's working directory — which is usually not what you want.


See also

  • GitHub Actions and Automated Code Review — how headless mode is used in PR workflows
  • GitLab CI/CD and Headless Automation — the same pattern for GitLab
  • Git, Commits, and Pull Requests — git operations that are convenient to automate via -p
  • Hooks — Lifecycle Events — PreToolUse/PostToolUse events that work in headless mode too
  • Claude Agent SDK: Building Agents Programmatically — when claude -p isn't enough and you need full programmatic control via Python/TypeScript
  • Cloud Agents: Web, Routines, and Background Tasks — an alternative to headless mode for long-running background tasks

Источники

  1. Claude Code CLI reference
  2. Run Claude Code programmatically (headless mode)