Claude Code Output Format JSON: Fields, jq & Python Parsing
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.
| Your situation | What to pick first | First 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 failure | json, then check subtype and is_error | /claude-code-cron-job-setup-run-automations-overnight/ |
| Long runs you want to watch live, or retry visibility | stream-json --verbose | Stream responses section |
| Budget-conscious, tracking spend per run | json, log total_cost_usd and usage | /claude-code-how-to-see-token-usage-and-reduce-it/ |
| You need typed fields (lists, flags) and not prose | json + --json-schema, read structured_output | Get structured output |
| You only want to read a saved output file nicely | VS Code's built-in JSON formatter | VS Code JSON docs |
Why each pick works:
- Beginner:
jsongives you one object, so onejqcall answers "did it work, and what did it say?" - Cron job: Claude Code returns a non-zero exit code when a run fails, but the JSON tells you how it failed (
error_max_turnsanderror_max_budget_usdcall for different fixes). - Live monitoring:
stream-jsonshowssystem/api_retryevents, so you can tell a stalled run apart from one that is waiting on a retry. - Budget:
total_cost_usdgives you a spend figure for every run without logging into a dashboard.
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):
| Field | Type | What it tells your script |
|---|---|---|
type | string | Always "result" for this object. In stream-json, use it to find the final line |
subtype | string | "success", "error_during_execution", "error_max_turns", "error_max_budget_usd" or "error_max_structured_output_retries" |
is_error | boolean | true when the run ended in an error state |
result | string or absent | The final text answer |
structured_output | any, optional | Present only if you passed --json-schema and the run produced valid output |
duration_ms | integer | Wall-clock time for the whole run |
duration_api_ms | integer | Time spent waiting on model API calls |
num_turns | integer | How many agent turns ran (compare it with your --max-turns) |
session_id | string | Pass to --resume to continue this conversation |
stop_reason | string or null | Why the model stopped |
total_cost_usd | number | Estimated spend for the conversation |
usage | object | Token counts (input, output, cache creation, cache read) |
| per-model usage | object | Per-model breakdown. Named modelUsage in the TypeScript reference, model_usage in the Python SDK |
permission_denials | array | Tool calls that were denied, which is useful in unattended runs |
errors | array, optional | Error 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
- 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
--continueor--resume, the run reports the conversation's whole total, including what earlier runs spent (headless docs). Summingtotal_cost_usdacross 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
resultfield.
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):
Event (type / subtype) | When it appears | Useful for |
|---|---|---|
system / init | First, unless hook or plugin-install events come before it | Model, tools, MCP servers, plugins, plugin_errors, mcp_server_errors |
assistant, user | Every turn, including subagent messages (parent_tool_use_id set) | Logging tool calls and results |
stream_event | Only with --include-partial-messages | Live text deltas |
system / api_retry | Before each retry of a retryable API error | attempt, max_retries, retry_delay_ms, error |
system / permission_denied | When a tool call is denied | Finding out which permissions are missing |
result | Last line | Same fields as the json format |
Steps: consume stream-json in a pipeline
- Run with
--output-format stream-json --verbose. Add--include-partial-messagesonly if you need token-level text. - Read stdout line by line. Each line is a complete JSON object, and the stream as a whole is not one JSON document.
- When you see
type == "system"withsubtype == "init", checkmcp_server_errorsandplugin_errors. Claude Code skips invalid--mcp-configentries and "the run continues and exits cleanly," so a non-empty array is your only signal (headless docs). - On
system/api_retry, logattemptanderror. Theerrorvalues includerate_limit,overloaded,billing_errorandauthentication_failed. - On
type == "result", apply the same success checks you'd apply to thejsonformat.
stream-json pitfalls
- Running
jq '.result'on the whole stream. jq applies the filter to every line, so you get a column ofnulls with the answer at the end. Filter withselect(.type == "result"). - No text deltas. Without
--include-partial-messages, you won't getstream_eventlines. You get whole messages only. - A slow reader holds the process open. If your consumer reads slowly, Claude Code waits for queued output to drain before it exits, "capped at 30 seconds" (headless docs).
- Subagent noise. Messages from subagents appear in the same stream. If you only want the main conversation, filter on
parent_tool_use_id == null.
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:
-r: a string result "will be written directly to standard output rather than being formatted as a JSON string with quotes."-j: "Like -r but jq won't print a newline after each output."-e: sets the exit status to 1 "if the last output value was either false or null," which makes jq usable as a test.-c: puts "each JSON object on a single line," which is handy for appending to a log.
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
- Quoting. Put jq programs in single quotes so the shell doesn't expand
$or!. jq -eandfalse.jq -e '.is_error'exits 1 whenis_errorisfalse, which is the healthy case. Test the condition you want to be true, as snippet 4 does.- Parsing twice. Each
claude -pcall is a new paid run. Capture the output in a variable or file first.
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
- Treating
final is Noneas 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.runfor long streams. It buffers everything until the process exits, so you lose the live view. UsePopenand 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 withjq '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/.
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:
- Save the output:
claude -p "..." --output-format json > run.json. - In VS Code, choose File > Open File... and select
run.json. - Check that the status bar in the bottom-right says JSON. If it says Plain Text, click it and pick JSON.
- Press the Format Document shortcut, or right-click > Format Document.
- 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
- stream-json files aren't JSON documents. A
.jsonlfile with many objects won't format as one document. Convert it to an array first, for examplejq -s '.' run.jsonl > run.json, or pull out just the result line withjq 'select(.type=="result")' run.jsonl > result.json. - JSON with Comments warnings. In JSONC mode, VS Code "accepts trailing commas, but they are discouraged and the editor will display a warning" (VS Code JSON docs). Claude Code output contains neither comments nor trailing commas, so a warning there usually means the file was edited by hand.
- "There is no formatter for 'plaintext' files." The language mode is wrong. Fix it as in step 3.
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.
| Built-in JSON formatter | Prettier extension (esbenp.prettier-vscode) | |
|---|---|---|
| Install | Ships with VS Code | Marketplace extension. Free, listed with over 72 million installs (Marketplace, as of October 2026; check the official page) |
| Config | VS Code settings only | .prettierrc in the repo, shared with CI |
| Defaults | Follows your editor indent settings | printWidth 80, tabWidth 2 (Prettier options) |
| Same rules in CLI/CI | No | Yes (npx prettier --check) |
| Good for | Reading run.json, quick fixes | Teams 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:
- Open the Command Palette (⇧⌘P on Mac, Ctrl+Shift+P on Windows/Linux).
- Run Preferences: Open User Settings (JSON), or Preferences: Open Workspace Settings (JSON) to apply it to one project only.
- 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
}
}
- 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.
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.
| Symptom | Likely cause | Fix |
|---|---|---|
jq: parse error or JSONDecodeError | You used 2>&1, so stderr text landed in the JSON stream | Send stderr to its own file: 2>>claude.err |
| Alert never fires but the work wasn't done | Script 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 upward | Summing 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 null | Run declined or failed schema retries (error_max_structured_output_retries) (Python SDK reference) | Check the field exists, alert on that subtype |
| MCP tools silently missing | Invalid --mcp-config entry skipped, run "exits cleanly" (headless docs) | Fail on non-empty mcp_server_errors in system/init |
| No result line at all | Process killed with SIGTERM (exit 143) (headless docs) | Treat a missing result as failure, and send SIGINT for graceful stops |
| Run hangs after the answer | Waiting 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 rejected | Piped 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:
- Run
claude -p "Say hi" --output-format json | jq 'keys'and save the key list. That's the schema for your installed version. - 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. - If any job uses
--mcp-config, switch it tostream-json --verboseand fail on a non-emptymcp_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:
- /claude-code-cron-job-setup-run-automations-overnight/: scheduling the runs you're now parsing
- /ai-agent-monitoring-in-claude-code-catch-silent-failures/: what to check beyond
is_error - /claude-code-how-to-see-token-usage-and-reduce-it/: turning
usagenumbers into savings
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
- Run Claude Code programmatically (headless docs)
- Claude Code CLI reference
- Agent SDK reference: Python (ResultMessage)
- Agent SDK reference: TypeScript
- Claude Code GitHub Actions
- Claude pricing
- jq manual
- Python json module documentation
- VS Code: JSON editing
- VS Code: Basic editing (Formatting)
- Prettier options
- Prettier extension on VS Code Marketplace
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.