A command-line tool should treat machine-readable mode as a stable API contract, not a convenience formatter. In that mode, stdout should contain only the primary structured payload, stderr should contain diagnostics only, exit codes should distinguish broad outcome classes, and the payload format should stay locale-independent and versioned. If a script must guess whether a warning line, color code, or progress bar is data, the contract is broken. The emphasis on clean interfaces aligns with the UNIX-style composability described by Command Line Interface Guidelines and the POSIX shell-and-utilities model in The Open Group Base Specifications Issue 7.
What belongs on stdout, stderr, and the exit status?
Guarantee one responsibility per channel. Stdout is for machine-consumable result data only. Stderr is for warnings, retries, stack traces, and human guidance. Exit status communicates the high-level class of outcome: success, user error, runtime dependency failure, interruption, or partial completion.
Exit code alone is not enough for automation once partial success exists. If a command processes ten resources and updates eight, your contract should say whether that is exit 0 with a partial field, a dedicated nonzero code with a result payload, or a hard failure with no payload. Pick one and document it.
Which formatting invariants actually matter?
In machine-readable mode, define invariants that do not depend on a terminal. Use a fixed encoding such as UTF-8. Keep field names and enumerated values locale-independent even if human mode is translated. Disable ANSI color, spinners, tables, headers, and pagers on stdout. If progress reporting matters, send it to stderr or require an explicit side channel.
Also document nullability and omission rules. A missing field, a present null, and an empty list mean different things in scripts. Deterministic key ordering is optional for JSON parsers, but deterministic record ordering is still useful when users diff outputs or run golden tests.
How should you version the output contract?
Version the output contract separately from the binary version. A CLI can release bug fixes without changing its machine contract, and it can ship a breaking schema in a major format version even if the executable is otherwise compatible.
A practical pattern is a top-level format identifier:
{"format":"inventory/v1","status":"partial","items":[...],"errors":[...]}
Then make an explicit promise: additive fields may appear within v1, existing fields keep meaning, and removals or semantic changes require v2. That gives script authors something concrete to pin.
What failures break automation in practice?
A common failure is mixing human text into stdout:
Fetching latest data...
{"users":12}
That is parse-hostile even though a person can read it. The same applies to locale-formatted numbers like 12,5 versus 12.5, dates rendered in local formats, and color escapes embedded in pipelines. Another failure is abrupt termination after writing a prefix of JSON. If the tool streams results, define record boundaries clearly; if it writes one final document, write it only after the command has reached the boundary where the result is complete enough to serialize.
Decision checklist:
- Does stdout contain only one documented machine format?
- Can stderr be ignored without losing required data?
- Are field meanings independent of locale and TTY?
- Is partial success represented explicitly?
- Is there a format version or schema identifier?
- Do tests cover pipes, TTYs, and non-default locales?
How should you test the contract?
Test at the subprocess boundary, not just inside formatter functions. Assert that piped stdout is valid structured output, stderr can be noisy without corrupting stdout, and TTY detection does not change machine mode. Run a locale matrix and interruption cases. Golden tests should freeze semantic structure, but avoid overfitting irrelevant whitespace unless whitespace is part of the contract.
Two follow-up questions:
Should warnings ever go to stdout in JSON mode?
No. Put them on stderr, or include them inside a documented warnings field in the payload.
Should machine-readable mode imply exit 0 whenever any data is returned?
No. Partial or degraded results may still need a distinct nonzero code if automation must branch on that condition.
Next step: write a one-page output contract for one command, then add subprocess tests that enforce it before you expose --json as stable.
Reviewed: 2026-09-05
Sources are linked throughout this guide. Product capabilities can change; consult the linked documentation for your deployment.
Read our editorial approach ↗