UnattendedClaude Code Guides

Claude Code Output Format JSON: Fields, jq & Python Parsing

By Aoba Works · Published · Sources checked · 18 min read

How this guide was made: drafted with AI assistance from the official sources linked below, then passed an automated editorial check before publishing. Our process

To get JSON from Claude Code, run claude -p "your prompt" --output-format json. When the run finishes, it prints one JSON object with the answer in result and run metadata next to it: type, subtype, is_error, duration_ms, num_turns, session_id, total_cost_usd and usage. The official docs call this format "structured JSON with result, session ID, and metadata". If you want events while the run is still going, use --output-format stream-json. It prints one JSON object per line, and the last line is the same result object.

One thing to know before you write a parser: many older posts mention a cost_usd field. The current Agent SDK reference calls the cost field total_cost_usd. If your script reads .cost_usd, it will quietly get null back.

Which option is right for you?

Use json if you only need the final outcome. Use stream-json if you need progress, tool calls or retry events while the run is in progress.

Which option is right for you?
Your situationWhat to pick firstFirst page to open
Beginner, first headless script--output-format json plus jq -r '.result'Run Claude Code programmatically
Nightly cron job that must alert on failurejson, then check subtype and is_error/claude-code-cron-job-setup-run-automations-overnight/
Long runs you want to watch live, or retry visibilitystream-json --verboseStream responses section
Budget-conscious, tracking spend per runjson, log total_cost_usd and usage/claude-code-how-to-see-token-usage-and-reduce-it/
You need typed fields (lists, flags) and not prosejson + --json-schema, read structured_outputGet structured output
You only want to read a saved output file nicelyVS Code's built-in JSON formatterVS Code JSON docs

Why each pick works:

What does --output-format json return in Claude Code?

--output-format json returns one result object when the run ends. That object holds the final text in result, plus status, timing, cost and token metadata.

The headless docs describe the payload and say it includes "total_cost_usd and a per-model cost breakdown" (headless docs). The full field list comes from the ResultMessage definition in the Python Agent SDK reference, which describes the same result message the CLI prints (as of October 2026):

What does --output-format json return in Claude Code?
FieldTypeWhat it tells your script
typestringAlways "result" for this object. In stream-json, use it to find the final line
subtypestring"success", "error_during_execution", "error_max_turns", "error_max_budget_usd" or "error_max_structured_output_retries"
is_errorbooleantrue when the run ended in an error state
resultstring or absentThe final text answer
structured_outputany, optionalPresent only if you passed --json-schema and the run produced valid output
duration_msintegerWall-clock time for the whole run
duration_api_msintegerTime spent waiting on model API calls
num_turnsintegerHow many agent turns ran (compare it with your --max-turns)
session_idstringPass to --resume to continue this conversation
stop_reasonstring or nullWhy the model stopped
total_cost_usdnumberEstimated spend for the conversation
usageobjectToken counts (input, output, cache creation, cache read)
per-model usageobjectPer-model breakdown. Named modelUsage in the TypeScript reference, model_usage in the Python SDK
permission_denialsarrayTool calls that were denied, which is useful in unattended runs
errorsarray, optionalError details when the run failed

The Python reference says it plainly: "The subtype field determines which other fields are populated." So read subtype first and the other fields after it.

An illustrative json result

Here is the shape of the output, with example values made up for illustration and some fields left out:

{
  "type": "result",
  "subtype": "success",
  "is_error": false,
  "duration_ms": 18432,
  "duration_api_ms": 15110,
  "num_turns": 4,
  "result": "Found 2 failing tests in auth.spec.ts and fixed both.",
  "session_id": "example-session-id",
  "total_cost_usd": 0.0412,
  "usage": { "input_tokens": 1200, "output_tokens": 640 }
}

The exact set of keys can change between Claude Code versions. Before you hard-code anything, run this once on your machine:

claude -p "Say hi" --output-format json | jq 'keys'

Three facts about cost and exit codes

Getting typed data with --json-schema

If your monitor needs fields and not prose, add --json-schema. The validated data shows up in structured_output. The docs warn that when a run ends without valid structured output, "the response has no structured_output field" (headless docs). Check that the field exists before you read it. If the schema itself is invalid, claude exits with Error: --json-schema is not a valid JSON Schema.

How do you use Claude Code output format stream-json?

Claude Code's stream-json format prints newline-delimited JSON events while the agent works, and finishes with a result line. Read it one line at a time.

The documented command for live token streaming is:

claude -p "Explain recursion" --output-format stream-json --verbose --include-partial-messages

"The last line of the stream is a result message with the final response text, cost, and session metadata." Before that line you'll see these event types (from the headless docs):

How do you use Claude Code output format stream-json?
Event (type / subtype)When it appearsUseful for
system / initFirst, unless hook or plugin-install events come before itModel, tools, MCP servers, plugins, plugin_errors, mcp_server_errors
assistant, userEvery turn, including subagent messages (parent_tool_use_id set)Logging tool calls and results
stream_eventOnly with --include-partial-messagesLive text deltas
system / api_retryBefore each retry of a retryable API errorattempt, max_retries, retry_delay_ms, error
system / permission_deniedWhen a tool call is deniedFinding out which permissions are missing
resultLast lineSame fields as the json format

Steps: consume stream-json in a pipeline

  1. Run with --output-format stream-json --verbose. Add --include-partial-messages only if you need token-level text.
  2. Read stdout line by line. Each line is a complete JSON object, and the stream as a whole is not one JSON document.
  3. When you see type == "system" with subtype == "init", check mcp_server_errors and plugin_errors. Claude Code skips invalid --mcp-config entries and "the run continues and exits cleanly," so a non-empty array is your only signal (headless docs).
  4. On system/api_retry, log attempt and error. The error values include rate_limit, overloaded, billing_error and authentication_failed.
  5. On type == "result", apply the same success checks you'd apply to the json format.

stream-json pitfalls

How do you parse Claude Code JSON output with jq?

Pipe claude -p ... --output-format json into jq -r '.result' for the text, or into jq field paths such as .total_cost_usd and .is_error for monitoring values.

The jq flags that matter here, quoted from the jq manual:

Copy-paste jq snippets

Save the output once, then read it as many times as you need. Each of these is an example; adjust paths to your setup:

# 1. Run once, keep the raw JSON
out=$(claude -p "Run the test suite and summarize failures" \
  --output-format json --allowedTools "Bash,Read")

# 2. Just the answer
echo "$out" | jq -r '.result'

# 3. Monitoring values in one line (tab-separated)
echo "$out" | jq -r '[.subtype, .is_error, .duration_ms, .num_turns, .total_cost_usd] | @tsv'

# 4. Fail the script unless the run succeeded
echo "$out" | jq -e '.subtype == "success" and .is_error == false' > /dev/null \
  || { echo "Claude run failed: $(echo "$out" | jq -r '.subtype')"; exit 1; }

# 5. Append a compact record to a JSONL log for later analysis
echo "$out" | jq -c '{ts: now, subtype, is_error, duration_ms, num_turns, total_cost_usd, session_id}' \
  >> ~/claude-runs.jsonl

# 6. Default a missing field instead of printing null
echo "$out" | jq -r '.structured_output // "NO_STRUCTURED_OUTPUT"'

Snippet 6 relies on jq's alternative operator //, which returns the right side when the left side is false or null (jq manual).

For stream-json, the docs filter text deltas like this:

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'

To keep only the final record from a stream, as an example:

claude -p "Nightly dependency audit" --output-format stream-json --verbose \
  | tee run.jsonl \
  | jq -c 'select(.type == "result") | {subtype, is_error, total_cost_usd}'

tee saves the full transcript, so you can debug later without re-running and paying again.

jq pitfalls

How do you parse Claude Code JSON output with Python?

For the json format, pass the subprocess stdout to json.loads(). For stream-json, call json.loads() on each line. In both cases, branch on subtype and is_error.

Python's json.loads handles one document. "If the data being deserialized is not a valid JSON document, a JSONDecodeError will be raised." That's why stream output needs a per-line loop.

Example: a minimal monitor for json output

This is an illustrative script. Replace alert() with whatever notifier you already use:

import json
import subprocess
import sys

def alert(msg: str) -> None:
    print(f"ALERT: {msg}", file=sys.stderr)  # swap for your webhook, mail, etc.

proc = subprocess.run(
    ["claude", "-p", "Summarize yesterday's error log in logs/app.log",
     "--output-format", "json", "--allowedTools", "Read"],
    capture_output=True, text=True, timeout=1800,
)

try:
    data = json.loads(proc.stdout)
except json.JSONDecodeError:
    alert(f"No valid JSON (exit {proc.returncode}). stderr: {proc.stderr[:500]}")
    sys.exit(1)

subtype = data.get("subtype")
if proc.returncode != 0 or data.get("is_error") or subtype != "success":
    alert(f"Run failed: subtype={subtype} turns={data.get('num_turns')}")
    sys.exit(1)

cost = data.get("total_cost_usd") or 0
if cost > 1.00:  # example threshold
    alert(f"Run cost ${cost:.2f}, above threshold")

print(data.get("result", ""))

Keep stdout and stderr separate (capture_output=True does that). The stderr text then shows up in your alert without breaking the JSON parse.

Example: reading stream-json line by line

import json, subprocess

cmd = ["claude", "-p", "Refactor utils.py and run tests",
       "--output-format", "stream-json", "--verbose"]

with subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True) as proc:
    final = None
    for line in proc.stdout:
        line = line.strip()
        if not line:
            continue
        event = json.loads(line)
        t, st = event.get("type"), event.get("subtype")
        if t == "system" and st == "init" and event.get("mcp_server_errors"):
            print("MCP config problem:", event["mcp_server_errors"])
        elif t == "system" and st == "api_retry":
            print(f"retry {event['attempt']}/{event['max_retries']}: {event['error']}")
        elif t == "result":
            final = event

if final is None:
    print("Stream ended without a result line (killed or crashed?)")
elif final.get("is_error"):
    print("Failed:", final.get("subtype"))

Python pitfalls

If you want the bigger picture on catching runs that "succeed" but did nothing useful, see /ai-agent-monitoring-in-claude-code-catch-silent-failures/.

How do you format a JSON file in VS Code?

Open the .json file and run Format Document. The shortcut is ⇧⌥F on Mac, Shift+Alt+F on Windows, or Ctrl+Shift+I on Linux, or you can right-click and choose Format Document.

That shortcut comes straight from the VS Code JSON docs: "You can format your JSON document using ⇧⌥F (Windows Shift+Alt+F, Linux Ctrl+Shift+I) or Format Document from the context menu."

Steps for a saved Claude Code output:

  1. Save the output: claude -p "..." --output-format json > run.json.
  2. In VS Code, choose File > Open File... and select run.json.
  3. Check that the status bar in the bottom-right says JSON. If it says Plain Text, click it and pick JSON.
  4. Press the Format Document shortcut, or right-click > Format Document.
  5. To format only part of the file, select it and use Format Selection (⌘K ⌘F on Mac, Ctrl+K Ctrl+F on Windows/Linux), per the VS Code basic editing docs.

Formatting pitfalls

VS Code JSON formatter: built-in or Prettier?

For reading Claude Code output, VS Code's built-in JSON formatter is enough. Prettier is worth adding only when a team needs the same formatting rules everywhere.

VS Code JSON formatter: built-in or Prettier?
Built-in JSON formatterPrettier extension (esbenp.prettier-vscode)
InstallShips with VS CodeMarketplace extension. Free, listed with over 72 million installs (Marketplace, as of October 2026; check the official page)
ConfigVS Code settings only.prettierrc in the repo, shared with CI
DefaultsFollows your editor indent settingsprintWidth 80, tabWidth 2 (Prettier options)
Same rules in CLI/CINoYes (npx prettier --check)
Good forReading run.json, quick fixesTeams committing JSON fixtures or config

Who Prettier is not for: if you just want to read the occasional output file, Prettier adds an extension, a config file and a second formatter that can fight with the built-in one. One limitation: once two formatters are installed, VS Code may ask you to pick a default, and formatting does nothing until you do.

Who the built-in formatter is not for: teams that want a pre-commit or CI check. It only runs inside the editor.

My pick for this audience is the built-in formatter for viewing output, plus jq . on the command line, which pretty-prints by default. Add Prettier only when JSON files go into a shared repo.

How do you auto-format JSON on save in Visual Studio Code?

To auto-format JSON in Visual Studio Code, turn on editor.formatOnSave and set a default formatter for the [json] language in settings.json.

VS Code documents three automatic triggers: editor.formatOnType, editor.formatOnSave ("Format a file on save") and editor.formatOnPaste (basic editing docs).

Steps:

  1. Open the Command Palette (⇧⌘P on Mac, Ctrl+Shift+P on Windows/Linux).
  2. Run Preferences: Open User Settings (JSON), or Preferences: Open Workspace Settings (JSON) to apply it to one project only.
  3. Add a per-language block. Example with the built-in formatter:
{
  "[json]": {
    "editor.defaultFormatter": "vscode.json-language-features",
    "editor.formatOnSave": true
  },
  "[jsonc]": {
    "editor.defaultFormatter": "vscode.json-language-features",
    "editor.formatOnSave": true
  }
}
  1. To use Prettier instead, swap the formatter ID for esbenp.prettier-vscode. The Marketplace page shows the same per-language pattern.
  2. Save the file, open a run.json, change something and save it. It should re-indent.

Two catches. Format-on-save doesn't run when a script writes the file. It only runs when you save in the editor. And workspace settings override user settings, so if nothing happens, check .vscode/settings.json in the repo.

What breaks when you pipe Claude Code JSON into monitoring scripts?

Four things break most pipelines: mixed stderr, exit-code-only checks, a missing result line, and misread cost.

What breaks when you pipe Claude Code JSON into monitoring scripts?
SymptomLikely causeFix
jq: parse error or JSONDecodeErrorYou used 2>&1, so stderr text landed in the JSON streamSend stderr to its own file: 2>>claude.err
Alert never fires but the work wasn't doneScript checks exit code only, or reads .cost_usd (null) (Python SDK reference)Check subtype == "success" and is_error == false, and use total_cost_usd
Cost charts jump upwardSumming total_cost_usd across --resume runs that report cumulative totals (headless docs)Log session_id and keep only the latest value per session
structured_output is nullRun declined or failed schema retries (error_max_structured_output_retries) (Python SDK reference)Check the field exists, alert on that subtype
MCP tools silently missingInvalid --mcp-config entry skipped, run "exits cleanly" (headless docs)Fail on non-empty mcp_server_errors in system/init
No result line at allProcess killed with SIGTERM (exit 143) (headless docs)Treat a missing result as failure, and send SIGINT for graceful stops
Run hangs after the answerWaiting on background work, 10-minute idle cap by default (headless docs)Set CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS or avoid background tasks in -p
Piped input rejectedPiped stdin is capped at 10MB (headless docs)Write the data to a file and reference its path in the prompt

Sources for the table rows: headless docs and the Python SDK reference.

The stderr row is the one people hit most often. The docs note that the "Waiting for background work to finish" message goes to stderr, and that with JSON output it prints only when stdout isn't a terminal, "so the JSON your script reads never carries it" (headless docs). If you merge the two streams yourself, that guarantee is gone.

For CI and cron, the docs recommend --bare, which skips local hooks, plugins, MCP servers and CLAUDE.md, so every machine gets the same system/init (headless docs). It needs ANTHROPIC_API_KEY because bare mode doesn't use your subscription login. The headless flags and auth trade-offs are covered in /claude-code-headless-mode-flags-auth-and-cost-2026/. If your alerts keep showing rate_limit retries, see /claude-code-rate-limit-reached-retry-setup-for-cron-jobs/.

Next steps

Three things you can do today:

  1. Run claude -p "Say hi" --output-format json | jq 'keys' and save the key list. That's the schema for your installed version.
  2. Wrap your existing cron command with jq snippet 4 (fail unless subtype == "success") and snippet 5 (append a JSONL record). That gives you an alert and a cost log in about ten lines.
  3. If any job uses --mcp-config, switch it to stream-json --verbose and fail on a non-empty mcp_server_errors.

If you'd rather not build and maintain this yourself: Claude Code's official GitHub Actions integration runs headless jobs inside GitHub, so run status and logs live in the Actions UI and you don't need your own parser. It suits people whose recurring work already lives in a GitHub repo. It's a poor fit for jobs that need files on your own server, or for anyone who wants per-run cost alerts (you'd still parse the JSON for those). Costs are your GitHub Actions minutes plus your Claude usage, through an API key or a plan. Claude Pro is listed at "$20 if billed monthly" and Max "From $100 Per month" on Claude's pricing page (as of October 2026; check the official page). For scripted --bare runs, remember they need an API key and won't use your subscription login.

Related guides on this site:

FAQ

How do I format a JSON file in VS Code?

Open the file, check that the status bar language mode is JSON, then press ⇧⌥F on Mac, Shift+Alt+F on Windows or Ctrl+Shift+I on Linux. You can also right-click and choose Format Document. For Claude Code stream-json files, convert to an array first with jq -s '.', because a multi-object file isn't one JSON document.

Which VS Code JSON formatter should I use?

For reading and occasionally fixing JSON, use the built-in formatter that ships with VS Code. Pick Prettier if your team commits JSON files and wants the same rules checked in CI. Running both without setting editor.defaultFormatter leads to "which formatter?" prompts.

What is Claude Code output format stream-json?

It's a newline-delimited JSON format. Each line is an event (system/init, assistant, user, system/api_retry, optional stream_event deltas), and the final line is a result object with the answer, cost and session metadata. The docs use it with --verbose, and with --include-partial-messages if you want token-level text.

How do I format JSON in Visual Studio Code automatically?

Add "editor.formatOnSave": true and an "editor.defaultFormatter" inside a "[json]" block in your settings JSON. Formatting happens when you save in the editor. Files written by scripts stay as they are until you open and save them.

Sources

Spotted something out of date or wrong? Tell us at shigoto.ai.note@gmail.com. Guides are re-checked against official sources on a rolling schedule.