$ sudo teach IT

Текстовый ответ хорош для человека и плох для скрипта. Попробуйте разобрать свободную речь регулярным выражением: сегодня Claude напишет «Найдено 3 проблемы», завтра — «Обнаружил три проблемы», и ваш парсер сломается. Плюс из текста не узнать ни сколько стоил запуск, ни сколько ходов он занял, ни идентификатор сессии для продолжения.

Claude Code решает это на двух уровнях. Первый — формат json: ответ приходит в конверте с метаданными, где текст лежит в отдельном поле, а рядом идут стоимость, число ходов и идентификатор сессии. Второй — JSON Schema: вы описываете структуру, которую хотите получить, и данные приходят в готовом виде, без парсинга текста вообще.

После этого урока вы будете уметь разбирать ответ через jq, задавать схему вывода, считать стоимость запусков и понимать разницу между полями result и structured_output.

Что уже умеем / что добавим

В прошлом уроке мы освоили claude -p и пайпы. Теперь заменим текстовый вывод на структурированный: разберём три значения --output-format, флаг --json-schema, поля JSON-ответа и потоковый режим stream-json.

Что вы узнаете в рамках статьи

  • Чем отличаются форматы text, json и stream-json
  • Какие поля есть в JSON-ответе и что означает каждое
  • Как задать схему вывода флагом -json-schema
  • В чём разница между полями result и structured_output
  • Как извлекать нужные значения утилитой jq

Ключевые термины

  • -output-format — флаг выбора формата вывода: text, json или stream-json.
  • -json-schema — флаг, задающий JSON Schema, которой должен соответствовать структурированный вывод.
  • result — поле JSON-ответа с текстовым результатом работы.
  • structured_output — поле JSON-ответа, куда попадают данные по заданной схеме.
  • total_cost_usd — оценка стоимости запуска в долларах, вычисляемая на стороне клиента.
  • jq — утилита командной строки для разбора JSON.

Три формата вывода

Флаг --output-format принимает три значения:

  • text — по умолчанию, обычный текст;
  • json — структурированный JSON с результатом, идентификатором сессии и метаданными;
  • stream-json — построчный JSON для потоковой обработки в реальном времени.

Начнём со второго:

claude -p "Кратко опиши этот проект" --output-format json

Текстовый результат окажется в поле result, а вокруг него — метаданные. Ключевые поля итогового сообщения:

Поле Что означает
type Тип сообщения, для итогового — result
subtype success либо один из вариантов ошибки
is_error Завершился ли запуск ошибкой
result Текстовый результат (есть при успешном завершении)
session_id Идентификатор сессии для --resume
num_turns Число агентных ходов
duration_ms Общая длительность запуска
total_cost_usd Оценка стоимости запуска
usage Суммарный расход токенов

Достать нужное поле проще всего через jq:

# Только текст результата
claude -p "Кратко опиши этот проект" --output-format json | jq -r '.result'

# Идентификатор сессии, чтобы продолжить позже
session_id=$(claude -p "Начни ревью" --output-format json | jq -r '.session_id')
claude -p "Продолжи ревью" --resume "$session_id"

Важное про total_cost_usd: это клиентская оценка, а не биллинговые данные. Она считается локально по таблице цен, зашитой в сборку, и может расходиться с реальным счётом — при изменении цен, при неизвестной клиенту модели, при особых правилах тарификации. Использовать её стоит для мониторинга и приблизительного бюджетирования, но не для выставления счетов и не для финансовых решений; для этого есть Usage and Cost API и страница расхода в консоли.

Схема вывода: данные вместо текста

Формат json даёт конверт, но внутри result по-прежнему свободный текст. Следующий шаг — описать нужную структуру.

claude -p "Извлеки имена основных функций из auth.py" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}'

Результат по схеме попадёт в поле structured_output — не в result. Это ключевая деталь, на которой спотыкаются: в result остаётся текстовый ответ, а данные по схеме лежат отдельно.

claude -p "Извлеки имена функций из auth.py" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
  | jq '.structured_output'

Схему удобно держать в файле, если она большая. И два ограничения, о которых стоит знать заранее:

  • Если переданное значение — не валидная JSON Schema, claude завершится с ошибкой Error: --json-schema is not a valid JSON Schema и диагностикой валидатора.
  • Ключевое слово format (например, "format": "email") принимается, но трактуется как аннотация и не проверяется. Рассчитывать на него как на валидацию нельзя.

Флаг --json-schema работает только вместе с --output-format json.

Потоковый режим

Третий формат нужен, когда результат хочется показывать по мере генерации, а не ждать конца:

claude -p "Объясни рекурсию" --output-format stream-json --verbose --include-partial-messages

Каждая строка — отдельный JSON-объект с событием. Последняя строка потока — сообщение result с финальным текстом, стоимостью и метаданными сессии.

Отфильтровать из потока только текстовые дельты можно тем же jq:

claude -p "Напиши стихотворение" --output-format stream-json --verbose --include-partial-messages | \
  jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'

Флаг -r выводит строки без кавычек, -j склеивает их без переводов строк, поэтому текст льётся непрерывно.

Ещё поток полезен для контроля запуска. Первое событие — system/init с метаданными сессии: модель, инструменты, MCP-серверы, загруженные плагины. В нём есть два поля, которые превращают запуск в CI-гейт:

  • plugin_errors — ошибки загрузки плагинов; ключ отсутствует, если ошибок нет;
  • mcp_server_errors — записи -mcp-config, отброшенные при валидации; ключ тоже отсутствует, когда всё в порядке.

То есть пайплайн может падать при непустом массиве, а не молча работать с наполовину загруженной конфигурацией. Это важно потому, что при невалидной конфигурации сервера запуск продолжается и завершается штатно — без проверки вы просто не узнаете о проблеме.

Собираем скрипт целиком

Соберём всё вместе: запуск с проверкой статуса, разбором результата и учётом стоимости.

#!/usr/bin/env bash
set -euo pipefail

out=$(git diff origin/main | claude --bare -p \
  --permission-mode dontAsk \
  --output-format json \
  --json-schema '{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"file":{"type":"string"},"problem":{"type":"string"}},"required":["file","problem"]}}},"required":["issues"]}' \
  "Найди проблемы в этом диффе")

# Проверяем, что запуск не завершился ошибкой
if [ "$(echo "$out" | jq -r '.is_error')" = "true" ]; then
  echo "Запуск завершился ошибкой" >&2
  exit 1
fi

# Считаем найденные проблемы
count=$(echo "$out" | jq '.structured_output.issues | length')
cost=$(echo "$out" | jq -r '.total_cost_usd')

echo "Найдено проблем: $count, оценка стоимости: $cost USD"
echo "$out" | jq -r '.structured_output.issues[] | "\(.file): \(.problem)"'

[ "$count" -eq 0 ]

Последняя строка задаёт код завершения: скрипт вернёт 0, только если проблем не нашлось. Так шаг пайплайна становится настоящим гейтом.

Частые ошибки

  • Искать результат схемы в поле result. При использовании -json-schema данные попадают в structured_output, а в result остаётся текстовый ответ. Обращайтесь к .structured_output.
  • Считать total_cost_usd биллинговыми данными. Это клиентская оценка по зашитой таблице цен, она может расходиться со счётом. Для точных цифр есть Usage and Cost API и страница расхода.
  • Рассчитывать на format в схеме как на валидацию. Ключевое слово принимается, но трактуется как аннотация и не проверяется. Проверяйте значения сами.
  • Не проверять plugin_errors и mcp_server_errors в CI. При невалидной конфигурации сервера запуск продолжается и завершается штатно, поэтому без явной проверки поломка останется незамеченной.

Наиболее частые вопросы

  • Можно ли использовать -json-schema без -output-format json? — Нет, флаг работает только вместе с форматом json.
  • Что будет при невалидной схеме? — Claude Code завершится с ошибкой Error: --json-schema is not a valid JSON Schema и покажет диагностику валидатора.
  • Где взять session_id для продолжения запуска? — Из JSON-ответа: jq -r '.session_id'. Потом передайте его во флаг -resume.
  • Что находится в последней строке потока stream-json? — Сообщение result с финальным текстом, стоимостью и метаданными сессии.
  • Как проверить в CI, что плагин или MCP-сервер загрузился? — По полям plugin_errors и mcp_server_errors в событии system/init. Ключей нет, когда ошибок нет, поэтому гейт может падать при непустом массиве.

Итоги

Прекрасно — теперь ваши скрипты не гадают по тексту, а читают данные. Вы знаете три формата вывода и когда какой уместен, разбираете поля JSON-ответа, задаёте структуру через --json-schema и помните главное отличие: данные по схеме живут в structured_output, а не в result. Вы также умеете вытаскивать стоимость и идентификатор сессии и превращать запуск Claude Code в честный CI-гейт с кодом завершения.

Пара оговорок из этого урока стоит того, чтобы их запомнить: оценка стоимости приблизительна, а format в схеме не проверяется. Обе экономят нервы при отладке.

Дальше меняем угол зрения. Мы автоматизировали разовые запуски — а что, если задачу нужно повторять? На следующем уроке разберём /loop: повторение промпта внутри открытой сессии.

На следующий урок