Output and Exit Codes
The JSON envelope, what goes to stdout versus stderr, and the exit codes scripts and agents branch on.
The CLI is designed so machines can parse results without scraping progress text. Once you know where data goes and what exit codes mean, scripts and agents get much easier to trust.
In this article, we will separate stdout from stderr, read a postsale run envelope, use streaming mode safely, and recover from common exit codes.
Before We Begin
This article assumes that you have already run a successful command such as postsale account whoami or postsale carriers list.
The basic rule
| Stream | What belongs there |
|---|---|
| stdout | Success data: JSON, NDJSON, or one run envelope |
| stderr | Errors, progress, tool breadcrumbs, confirmation prompts, spinner |
| exit code | Overall outcome class (success, auth, confirmation, network, and more) |
There is no --json flag on postsale run. Non-stream run mode writes exactly one envelope to stdout (a contract the test suite enforces); the only exception is preflight failure, where stdout stays empty.
Direct commands
Here's how a typical read looks:
postsale orders search --query "last week" > result.jsonPretty output for humans:
postsale orders search --query "last week" --humanStream every page as NDJSON:
postsale orders search --query "last week" --allOn failure, stderr carries a structured envelope:
{
"error": {
"code": "...",
"message": "...",
"details": {},
"request_id": "...",
"retry_after": 12
}
}The envelope from postsale run
When the agent finishes (or hits a handled stop), stdout gets at most one JSON line: the completion envelope.
{
"summary": "You have 3 carriers: USPS, FedEx, UPS.",
"data": {},
"tool_calls": [
{
"name": "get_carriers",
"input": {},
"output": [],
"is_error": false,
"duration_ms": 234
}
],
"turns": 2,
"stop_reason": "end_turn",
"provider": "anthropic",
"model": "claude-opus-5"
}You may also see:
warningsuch asmax_turns_reachedsimulated_effects/confirmed_effects(especially with dry-run)- On a safety gate:
datashaped like{ "blocked": true, "code", "tool", "gate", "next" }
Capture cleanly:
postsale run "list my carriers" > envelope.jsonProgress never belongs in that file.
When stdout is empty
Preflight failures (missing LLM credential, malformed configuration, unknown --continue id) produce empty stdout, a structured error on stderr, and a non-zero exit. Treat that combination as "no envelope was produced."
Exceptions:
postsale run --examplesprints a bare JSON array of sample intents--interactiveandpostsale continueemit one envelope per user turn
Streaming mode
postsale run --stream "list my carriers" 2>/dev/nullEvery stdout line is one NDJSON event with an event field (tool_call_start, tool_call_end, text, complete, and parallel batch events when used). Ctrl-C emits aborted and exits 130.
Exit codes you will actually use
| Code | Meaning | Typical next step |
|---|---|---|
| 0 | Success | Parse stdout |
| 64 | Usage or confirmation required | Fix flags; pass --yes when intentional |
| 65 | Bad JSON input | Fix the body |
| 66 | Input file missing | Fix the path |
| 75 | Temporary / network (doctor failed checks; warnings too under --pre-flight) | Retry or fix warnings |
| 78 | Configuration or allowlist refusal | Fix config |
| 100 | Auth | Login or token |
| 101 | Subscription inactive | Human billing path |
| 102 | Validation | Read error.details |
| 103 | Rate limited | Wait retry_after |
| 104 | Not found (404) | Verify the id with a search |
| 106 | Request timeout | Retry or raise POSTSALE_HTTP_TIMEOUT_MS |
| 130 | Stream aborted | User cancelled |
Full table: Exit codes. Agent recovery by error.code: agent/errors.md.
Good to Know
- Pipe or redirect stdout for data. Leave stderr visible while debugging.
- Do not scrape spinner or breadcrumb lines as JSON.
- Stream purity and non-stream envelope purity are enforced by CLI tests. Hosts can rely on them.
Additional Reading
- How the CLI works
- The agent loop
- Safety model
- AGENTS.md output contract section
On this page
Related