Headless-режим и скриптинг через CLI

Есть задачи, которым не нужен интерактивный диалог. Code review в CI, автоматическое объяснение build-ошибок, ночной аудит зависимостей — всё это просто скрипты, которые должны отработать и вернуть результат. Флаг -p (сокращение от --print) переводит Claude Code именно в этот режим: запрос → агентный цикл → вывод результата → выход.

Эта статья — о том, как встроить Claude Code в скрипты, пайплайны и CI/CD так, чтобы он работал как нормальный Unix-инструмент: читал stdin, писал в stdout, возвращал структурированный вывод и не требовал ничего лишнего.


-p: основной флаг headless-режима

Самый простой случай — задать вопрос о кодовой базе и получить ответ:

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

Агент прочитает файлы в текущей директории, ответит и завершится. В отличие от интерактивного режима — никакого REPL, никаких запросов разрешений в stdin. Это ключевое: в -p режиме Claude Code всё ещё выполняет агентный цикл (читает файлы, запускает команды — если разрешено), но делает это без пауз на подтверждение.

# Объяснить конкретный файл
claude -p "Объясни логику в src/parsers/csv.ts, особенно обработку quoted fields"

# Быстрый code review изменений
git diff HEAD~1 | claude -p "Сделай краткий code review этого diff"

# Продолжить предыдущую сессию в headless-режиме
claude -c -p "Теперь сосредоточься на database queries"
Проверь себя
Предположи: если запустить `claude -p 'Объясни проект'` без флага `--allowedTools` — сможет ли агент читать файлы в текущей директории?

Пайпы и композиция с Unix-утилитами

Headless-режим читает stdin — это открывает стандартные Unix-паттерны.

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

# Следить за логом и объяснять аномалии в реальном времени
tail -f app.log | claude -p 'Предупреди, если увидишь паттерны ошибок или аномалии'

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

Одно практическое ограничение: stdin ограничен 10 МБ. Если контент больше — Claude Code завершится с ошибкой. Для больших файлов правильнее указать путь в промпте, а не пайпить содержимое.

Композиция работает в обе стороны: вывод Claude Code тоже можно пайпить в другие утилиты:

# Получить список файлов с проблемами и отправить в grep
claude -p 'Перечисли файлы с потенциальными утечками памяти, по одному пути на строку' | \
  xargs grep -l 'malloc'
flowchart LR A["stdin / файл / git diff"] --> B["claude -p 'промпт'" --output-format json --allowedTools ... --bare] B --> C["Агентный цикл Read / Bash / Edit"] C --> D["stdout: JSON"] D --> E1["jq '.result'"] D --> E2[">> report.md"] D --> E3["| grep ERROR"]
flowchart LR
    A["stdin / файл / git diff"] --> B["claude -p 'промпт'"
--output-format json
--allowedTools ...
--bare]
    B --> C["Агентный цикл
Read / Bash / Edit"]
    C --> D["stdout: JSON"]
    D --> E1["jq '.result'"]
    D --> E2[">> report.md"]
    D --> E3["| grep ERROR"]
Headless-режим как строительный блок пайплайна: Claude Code читает из stdin, выполняет агентный цикл, пишет JSON в stdout — как любая Unix-утилита

Форматы вывода: text, json, stream-json

По умолчанию -p возвращает plain text — удобно для чтения, но неудобно для парсинга в скриптах. Флаг --output-format меняет это.

text — стандартный текст, как в терминале.

json — структурированный объект с метаданными:

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

Вывод выглядит примерно так:

{
  "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
  }
}

Поле total_cost_usd позволяет отслеживать расходы на уровне скрипта — полезно в CI для cost tracking без обращения к дашборду.

Парсинг с jq:

# Только текст результата
claude -p "Summarize this project" --output-format json | jq -r '.result'

# Только стоимость
claude -p "Review this code" --output-format json | jq '.total_cost_usd'

stream-json — поток JSON-объектов, по одному на строку, по мере генерации. Каждый объект — событие агентного цикла. Используется когда нужно отображать прогресс или обрабатывать токены по мере поступления:

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'

Флаги --verbose и --include-partial-messages нужны, чтобы stream-json включал промежуточные события, а не только финальный результат.

Проверь себя
В чём разница между `--output-format json` и `--output-format stream-json`? В каком случае выберешь каждый?

Структурированный вывод с --json-schema

Комбинация --output-format json + --json-schema даёт гарантированно валидный JSON по заданной схеме. Агент завершит агентный цикл и вернёт структуру, соответствующую схеме — в поле structured_output:

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'

Это мощный паттерн для парсинга кодовых баз: агент изучает файлы, а вы получаете данные в виде массива/объекта — без регулярных выражений и хрупкого парсинга текста.


--bare: быстрый старт для CI

По умолчанию claude -p при старте загружает всё то же самое, что и интерактивная сессия: CLAUDE.md, хуки, скиллы, MCP-серверы, автопамять. В CI это часто лишнее — и медленное.

Флаг --bare пропускает автодискавери всего этого:

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

В bare-режиме доступны только базовые инструменты (Bash, чтение и редактирование файлов). Всё нужное подключается явно через флаги:

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

Важное практическое следствие: bare-режим не читает ANTHROPIC_API_KEY из keychain — аутентификация должна прийти через переменную окружения или --settings. В CI это обычно именно так и настроено.

> Согласно документации, --bare станет режимом по умолчанию для -p в будущих версиях.


Управление инструментами: --allowedTools

В headless-режиме запрос разрешения некому принять — поэтому нужно явно указать, что агент может делать без вопросов:

# Только чтение — агент не изменит ни одного файла
claude -p "Find all TODO comments in the codebase" \
  --allowedTools "Read,Bash(grep *),Bash(find *)"

# Чтение, редактирование и конкретные git-команды
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 — пишет файлы без запроса, но не shell-команды
claude -p "Apply lint fixes" --permission-mode acceptEdits

Синтаксис Bash(git diff *) — это permission rule syntax: пробел перед * важен. Bash(git diff *) разрешает любую команду, начинающуюся с git diff, а Bash(git diff*) без пробела — нет.

Проверь себя
Почему `Bash(git diff *)` с пробелом перед звёздочкой — это не то же самое, что `Bash(git diff*)`?

Лимиты: --max-turns и --max-budget-usd

Два флага, которые делают headless-запуски предсказуемыми по стоимости и времени:

# Не более 5 агентных шагов
claude -p "Investigate the failing tests" \
  --max-turns 5 \
  --output-format json

# Максимум $2 на запуск
claude -p "Refactor the auth module" \
  --max-budget-usd 2.00

Если агент достигает лимита --max-turns — завершается с ненулевым кодом возврата. Это удобно для CI: if claude -p ... --max-turns 10; then ... fi.

--max-budget-usd работает только с -p и учитывает total_cost_usd текущего запуска. Это грубый лимит, а не биллинговый контроль — для точного учёта используйте поле total_cost_usd из JSON-вывода.


Практические паттерны

Тайпо-линтер в 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 PR перед мержем:

#!/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'

Мульти-шаговый анализ с продолжением сессии:

# Первый запрос — сохраняем session_id
session_id=$(claude -p "Review this codebase for performance issues" \
  --output-format json | jq -r '.session_id')

# Последующие запросы в той же сессии
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'

Это особенно полезно, когда каждый шаг анализа опирается на контекст предыдущего — агент помнит, что уже нашёл.


Headless в CI/CD

Headless-режим — это основа интеграции Claude Code в GitHub Actions и другие CI-системы. Полноценный пайплайн рассматривается в GitHub Actions и автоматический code review, но базовый принцип прост:

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

Обратите внимание на --bare: без него агент попытается загрузить CLAUDE.md и MCP-серверы из рабочей директории CI-раннера — что обычно не то, что нужно.


See also