ai newsSa., 31. Okt.

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

Originally published at Unattended . 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

dev.toSamstag 17:27 · 31.10.12 Min. Lesezeit
Bild: dev.to

Posted on Oct 31 • Originally published at en.aobaworks.com

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.

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.

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

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.

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 } } Enter fullscreen mode Exit fullscreen mode 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' Enter fullscreen mode Exit fullscreen mode Three facts about cost and exit codes Cost is an estimate. The docs say: " Both figures are client-side estimates and can differ from your actual bill. " Use it for trends and alerts.

Don't use it for invoicing. Cost adds up across a session. If you use --continue or --resume, the run reports the conversation's whole total, including what earlier runs spent ( headless docs ).

Summing total_cost_usd across resumed runs therefore double-counts. Exit code plus stdout. Claude Code "exits with code 0 on success and a non-zero code when the run fails." Invalid flags go to stderr.

But " When a failure happens inside the run, such as missing authentication, Claude Code prints the failure as the result on stdout. " So stdout can contain an error message sitting in the result field. 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.

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 Enter fullscreen mode Exit fullscreen mode " 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 ):

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:

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"' Enter fullscreen mode Exit fullscreen mode 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' Enter fullscreen mode Exit fullscreen mode 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}' Enter fullscreen mode Exit fullscreen mode tee saves the full transcript, so you can debug later without re-running and paying again.

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.

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:. 2 f }, above threshold " ) print ( data. get ( " result ", "" )) Enter fullscreen mode Exit fullscreen mode Keep stdout and stderr separate ( capture_output=True does that).

The stderr text then shows up in your alert without breaking the JSON parse.

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 " )) Enter fullscreen mode Exit fullscreen mode Python pitfalls Treating final is None as success.

If something sends the process SIGTERM, Claude Code "exits with code 143" and "records no result" for the turn in progress ( headless docs ). A missing result line means failure. Using subprocess.run for long streams.

It buffers everything until the process exits, so you lose the live view. Use Popen and iterate. Python SDK names versus raw JSON names.

If you later switch to the Python Agent SDK, the per-model field becomes model_usage. In raw CLI JSON, check the key name with jq 'keys'. 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/.

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."

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.

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.

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 ).

{ "[json]": { "editor.defaultFormatter": "vscode.json-language-features", "editor.formatOnSave": true }, "[jsonc]": { "editor.defaultFormatter": "vscode.json-language-features", "editor.formatOnSave": true } } Enter fullscreen mode Exit fullscreen mode To use Prettier instead, swap the formatter ID for esbenp.prettier-vscode. The Marketplace page shows the same per-language pattern. 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.

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

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/.

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.

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.

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.

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.

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.

This article was created with the help of AI. Sources are linked inline; check the official documentation for the latest details.

Quelle: dev.to (dev.to). Original öffnen