Skip to main content

Output and errors

note

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.

Command--output valuesDefault
token balance, token transfer, token name, token symbol, token decimals, token approve, token allowancetext, json, json-formattedtext
tx fetch result, tx fetch meta, tx fetch eventsjson, json-formatted, xdr (values vary per subcommand)varies
tx fetch feetable, json, json-formattedtable
tx decodejson, json-formattedjson
tx encodesingle-base64, singlesingle-base64
network health, network infotext, json, json-formattedtext
network settingsxdr, json, json-formattedjson
ledger latest, ledger fetchtext, json, json-formattedtext
fees stats, fee-stats (deprecated)text, json, json-formattedtext
contract info interfacerust, xdr-base64, json, json-formatted (no text)rust
contract info meta, contract info env-metatext, xdr-base64, json, json-formatted (no rust)text
contract readstring, json, xdr (use xdr to parse, see below)string
contract inspect (deprecated)xdr-base64, xdr-base64-array, docsdocs
xdr decodejson, json-formatted, text, rust-debug, rust-debug-formattedtext
xdr encodesingle, single-base64, streamsingle-base64
eventspretty, plain, json, rawpretty
snapshot createjson onlyjson

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:

stellar tx new payment --source <IDENTITY> --destination <ADDRESS> --amount <N> --build-only --network <NET> \
| stellar tx sign --sign-with-key <IDENTITY> --network <NET> \
| stellar tx send --network <NET>

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:

$ stellar token balance --id nonexistent_bad_id --account agent-1 --network testnet --output json
{"error":{"type":"config","message":"contract not found: nonexistent_bad_id"}}

A well-formed but nonexistent C… address returns the same config type, not contract_not_found:

$ stellar token balance --id CBIELTK6…QDAAA --account agent-1 --network testnet --output json
{"error":{"type":"config","message":"contract not found: CBIELTK6…QDAAA"}}
$ stellar token transfer --id USDC:GBBD47IF… --from agent-1 --to doc-probe \
--amount 500000 --network testnet --output json
{"error":{"type":"invoke","message":"transaction simulation failed: HostError: Error(Contract, #13)\n\nEvent log (newest first):\n 0: [Diagnostic Event] … data:[\"trustline entry is missing for account\", GCBNIXOH…]\n…"}}

Known type values, from the token command source:

typeMeaning
sac_not_deployedThe Stellar Asset Contract for this classic asset has not been deployed yet. The error carries a hint pointing at stellar contract asset deploy --asset <ASSET> --source-account <IDENTITY>. --source-account is required on that command; the hint does not run without it.
contract_not_foundDefined in the token command source, but not reached through the token commands in testing. A nonexistent contract address returned config instead (see above). Do not rely on this type to detect a missing contract.
configA resolution or configuration problem: an unparseable --id, an unknown alias, or a well-formed but nonexistent C… contract address. This, not contract_not_found, is the type you actually get for a missing contract, with message: "contract not found: <ID>".
networkReserved for network-level failures. In testing, those returned invoke, so do not key retry logic on this value.
invalid_addressA --from, --to, --spender, or --account value is not a valid address. --account is the one that matters for reads: an unknown alias returns Account alias "<NAME>" not Found.
invokeThe contract call itself failed during simulation or submission. Check the message field's embedded diagnostic event log for the actual cause. Two you will meet often: Error(Contract, #13), "trustline entry is missing for account", for a classic asset with no trustline or no account at all; and Error(Contract, #6), "account entry is missing", for the native asset with no account at all. Both are invoke, so you cannot branch on type alone to tell them apart.
internalAn unexpected CLI-internal error.

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:

$ stellar strkey decode <MALFORMED_INPUT>
❌ error: Encoded text cannot have a 6-bit remainder.
$ stellar message verify "tampered message" --public-key GDCINM7O… --signature Z34PcX58…
❌ Signature invalid
❌ error: Signature verification failed

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.

$ stellar contract build --image docker.io/library/alpine:latest --pull # 1>out 2>err
latest: Pulling from library/alpine # all of this on stderr
a9986cd6f37d: Pull complete
Digest: sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6
Status: Downloaded newer image for alpine:latest

A pull that fails exits 1 with the engine's own message followed by the CLI's, both on stderr:

$ stellar contract build --image docker.io/stellar/definitely-not-a-real-image:v0 --pull
Error response from daemon: pull access denied for stellar/definitely-not-a-real-image, ...
❌ error: could not pull image docker.io/stellar/definitely-not-a-real-image:v0

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:

ExitMeansWhat an agent should do
0SuccessContinue
1Runtime failure. The invocation was well formed and something about the world caused it: an RPC error, a missing account, a failed simulation, a rejected submissionInspect, and retry only where the retry rules allow it
2Malformed invocation. An unrecognized subcommand, an unknown flag, an invalid value for a flagStop and escalate. Retrying the same command verbatim cannot succeed

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:

$ stellar message verify "agent-session-2026-09-09" \
--public-key GDCINM7OFENANN2Y73MSU74DWDZXLAXX7KMP3J4NH67IPKWWIUF33KAH \
--signature Z34PcX58nb+1CwrwM53uqYKk+/jcb5o5RfhhtsLEOpVQbiumbactxQpEnyTgDwMeUQbHMAqKb/StewzBhW7wDg==
ℹ️ Verifying signature against: GDCINM7OFENANN2Y73MSU74DWDZXLAXX7KMP3J4NH67IPKWWIUF33KAH
✅ Signature valid

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:

{"tx_hash":"cdbfa12f54d40d3c1f3b3a4c64cecd93f7a3dcc16574fe00bf1f2f557f65e78a","result":null}

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:

{"balance":"99999988251"}
{"decimals":7}
{"allowance":"250000000"}
{"name":"native"}
{"symbol":"native"}

Adding --decimal adds a second key rather than replacing the first, so do not write a parser that assumes exactly one:

{"balance":"9999.9988251","decimals":7}
{"allowance":"0.00001","decimals":7}

In text mode, the same reads return a bare value with no wrapping:

$ stellar token decimals --id USDC:GBBD47IF… --network testnet
7

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

$ … | stellar tx send --network testnet 2>/dev/null
{"status":"SUCCESS", …}

$ … | stellar tx send --network testnet 2>&1
ℹ️ Transaction hash is d9a2ae81f886c9b5cde637a282424eb1a8eb88c741aee5857a744cd29994ba…
{"status":"SUCCESS", …}

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:

stellar tx send --network <NET> 2>send.stderr

Before retrying any submit that appears to have failed, confirm it did not already land:

stellar tx fetch result --hash <HASH> --network <NET>

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:

$ stellar token transfer --id native --from agent-1 --to doc-probe --amount 10000000 --network testnet
ℹ️ Simulating transaction…
ℹ️ Signing transaction: 11659a52651dc9ba60dc422dc890abe17ab11c68ec4b976348398987c873df7e
🌎 Sending transaction…
✅ Transaction submitted successfully!
🔗 https://stellar.expert/explorer/testnet/tx/11659a52651dc9ba60dc422dc890abe17ab11c68ec4b976348398987c873df7e
11659a52651dc9ba60dc422dc890abe17ab11c68ec4b976348398987c873df7e

Capture it directly with command substitution:

TX=$(stellar token transfer --id native --from agent-1 --to <ADDRESS> --amount 10000000 --network testnet)

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:

stellar token balance --id native --account <ADDRESS> --network <NET> --output json

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:

stellar keys fund <NAME> --network testnet

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.