Skip to main content
A global --output json flag that works on every command is coming soon. Until then, --output is declared per command, and each command has its own values and default.
The stellar token family returns a typed, machine-parseable error envelope. Other commands report failure as text on stderr plus a non-zero exit code. Check the table below before you wire an agent to parse CLI output.

Commands that support --output

Where a command takes all three, text is human-readable, json is a compact single line, and json-formatted is that same JSON indented across multiple lines. Have your agent parse json and keep json-formatted for output a person reads. Beyond the table:
  • strkey decode and strkey encode have no --output flag, and they are not symmetric. strkey decode emits JSON: a decoded G... address comes back as {"public_key_ed25519": "<hex>"}. strkey encode takes that JSON as its input argument and emits a bare strkey string with no JSON wrapper at all.
  • tx send has no --output flag. Passing one exits 2. It always writes the indented multi-line shape, around 11 KB for a one-operation payment, so the compact json this page tells you to prefer is not available on the one command that returns a submission receipt. Parse the indented form, or take the hash from token transfer or the stderr signing line instead.
  • tx fetch fee defaults to table, not json, unlike every other tx fetch subcommand. Pass --output json explicitly if your agent needs to parse it.
  • On tx fetch, use --output json to parse. json-formatted on result, meta, fee, and events adds a human-readable header (Transaction Status and Transaction Ledger) above the JSON, so a JSON parser will not accept it. On the token family and ledger, json-formatted is plain indented JSON.
  • None of the 22 stellar tx new <operation> commands has an --output flag at all, and a successful submission writes nothing to stdout. See tx new has no machine-readable receipt below.
  • To parse contract read output, use --output xdr. See Parsing contract read below.

Parsing contract read

contract read --output json currently writes CSV rows with a JSON value in one field, not a single JSON document. For a machine-readable result, use --output xdr and decode it with stellar xdr decode, or parse the output as CSV.

tx new has no machine-readable receipt

stellar tx new payment --help, and every other tx new <operation> --help, has no --output flag. A submitted operation writes nothing to stdout on success. The transaction hash appears only on an ℹ️ Signing transaction: <HASH> stderr line, and --quiet removes that line along with everything else on stderr. Contrast this with token transfer, which prints the bare hash as its last stdout line and supports --output json directly. tx new has no equivalent of either. To get a parseable result from a tx new operation, split the pipeline and read the result from tx send, which does return JSON:
tx send’s JSON has top-level keys status, ledger, application_order, fee_bump, tx_hash, created_at, envelope, result, result_meta, and events. status is SUCCESS on success. The hash field is tx_hash, not hash.

The token family’s error envelope

stellar token commands wrap a failed call in a single JSON object with a type discriminator, so an agent can branch on the failure class without parsing prose:
A well-formed but nonexistent C… address returns the same config type, not contract_not_found:
Known type values, from the token command source: Both example calls above exit 1.

Everything else prints unstructured stderr text

Outside the token family, a failing command prints a plain ❌ error: ... line to stderr. There is no type field and no stable schema to parse. Two real examples:
An agent working against the rest of the CLI has to rely on the exit code and on matching substrings in stderr text. There is no discriminated error type outside token. This is the main obstacle to programmatic error handling in the CLI today, and it is worth flagging to your agent’s error-handling logic explicitly rather than assuming every command behaves like token.

A containerized build borrows stderr for the engine’s output

stellar contract build --image <IMAGE> shells out to the container engine, and --pull makes it pull first. The engine writes pull progress to its own stdout, and the CLI redirects that to stderr so the CLI’s stdout stays clean. Measured against Docker 29.8.0, stdout was zero bytes in every case below.
A pull that fails exits 1 with the engine’s own message followed by the CLI’s, both on stderr:
Without --pull there is no pull output at all: the build uses the local image and goes straight to the container, matching docker run. --quiet behaves here exactly as the section below describes, and this is the case where it costs most. A failed containerized build under -q writes zero bytes to stdout and zero bytes to stderr, so neither could not pull image nor any later build error reaches you. The exit code is the entire result. Confirmed live on both the failed-pull and failed-toolchain paths.

Exit codes

0 means success. Non-zero means failure, and the CLI separates two classes of failure: Measured across 104 error cases. The runtime direction held without exception: every runtime failure exited 1. The malformed direction held in 24 of 26 cases, and there is one known counterexample. xdr decode --type <invalid> and xdr encode --type <invalid> exit 1, not 2, and print the ❌ prefix. That single case defeats both discriminators at once. Every other invalid-flag-value case exits 2, including --input bogus on the same command. Otherwise the two classes are distinguishable in the text as well: exit 2 prints error: ... with no emoji, a runtime failure prints ❌ error: .... Treat all of this as current observed behavior rather than a guaranteed contract, since it comes from the argument parser rather than from anything the CLI promises. Branch on it, and still handle the general non-zero case. stellar message verify is a clean, verified example of both paths:
Exit code 0. On a tampered message, the same command exits 1 with ❌ Signature invalid followed by ❌ error: Signature verification failed. Check the exit code first. Don’t rely on matching the emoji-prefixed line alone, since that line’s exact text is not guaranteed stable across versions.

Success shapes

A mutating token command in JSON mode returns a fixed two-field object:
token approve returns the same shape. result is null on these calls; it is not populated with contract return data by transfer or approve. A read-only token command in JSON mode returns an object keyed by the command:
Adding --decimal adds a second key rather than replacing the first, so do not write a parser that assumes exactly one:
In text mode, the same reads return a bare value with no wrapping:
--decimal on token balance divides by the token’s own decimals before printing. Without it, you get the raw smallest-unit integer, which is what you want when feeding the value straight back into --amount on a later command.

Practical guidance

--quiet deletes stderr on a non-token command, error message included. It is not just quieter logging: on a failing non-token command, --quiet leaves you with the exit code and nothing else, zero bytes on stderr, confirmed live. Use --quiet only when the exit code is all you need. If you need to know why something failed, omit --quiet and capture stderr instead. The token family is safe either way, because its error is JSON on stdout, not stderr, and --quiet does not touch stdout. In JSON mode the token family writes nothing to stderr at all, measured on both a successful and a failing token transfer, so --quiet is redundant there rather than necessary. Combine --quiet with --output json on token commands for clean, parseable stdout with no informational logging (ℹ️ Simulating transaction…, 🌎 Sending transaction…, ✅ Transaction submitted successfully!, the 🔗 explorer link) mixed in. Do not reach for --quiet as a default on every command; it is a tradeoff, not a free clean-up. Never merge stderr into stdout when you intend to parse the result. This is the same class of hazard as --quiet, and it is the one that actually causes damage. stellar tx send writes an informational line to stderr (ℹ️ Transaction hash is <HASH>, or ℹ️ Signing transaction: <HASH> when the command is token transfer) and its JSON receipt to stdout, separately. 2>&1, the default habit of most agent tooling, interleaves the two streams and breaks the JSON:
The second form does not parse as JSON. An agent that treats that as a submit failure and retries resubmits a transaction that already succeeded, which spends twice. A failed parse is not evidence the transaction failed. One exception to the recovery route, and it is the mode most agents run in: token transfer --output json writes nothing at all to stderr, on success and on failure alike, measured on testnet. There is no ℹ️ Signing transaction: line to fall back on, so the hash exists only in stdout’s tx_hash. If that stdout is empty or unparseable, you have no local record of the hash. Do not retry. Re-read both balances, and go to Horizon if you need the hash itself. Redirect stderr separately, never merged, whenever you intend to parse stdout. Capture it to a file rather than discarding it, because for tx send the hash you need to confirm onchain state lives on that stderr line:
Before retrying any submit that appears to have failed, confirm it did not already land:
Only retry once you have confirmed the transaction is not already onchain. In text mode, a submitted transfer prints the bare transaction hash as the last line of stdout:
Capture it directly with command substitution:

Telling #13 from #6

#6 "account entry is missing" comes only from a native query and means the account does not exist on the network you queried. It says nothing about any other network. A live, funded mainnet account queried against testnet returns #6 byte-identically to an address that has never existed anywhere; this was measured against a mainnet account holding 4.3800772 XLM. #13 "trustline entry is missing for account" comes from a classic-asset query and is ambiguous in a second way: a funded account with no trustline and a nonexistent account both produce it. To disambiguate #13, query the native balance of the same address, and pass --network explicitly, because an unset network resolves to testnet silently:
A number means the account exists and only needs a trustline. #6 means check the network before you do anything else. Re-run the same query against the network the account is supposed to be on. Only once the address returns #6 on the network you actually meant is it a missing account, and only then does funding it make sense:
Running keys fund on a #6 you have not scoped creates a real, separate account on the wrong network. It then returns a balance, which reads as the problem being fixed. It is not: the funds you were looking for are still on the other network, and you have made a decoy.