Output channels (stdout / stderr)

June 27, 2026 · View on GitHub

This document defines the rules every Foundry CLI command (forge, cast, anvil, chisel, script, verify) must follow when writing to standard output and standard error.

Status: The macros, helpers, and routing primitives described here are implemented. The per-command stdout table at the bottom is the target contract that downstream migration PRs will bring each command into compliance with; it does not yet describe today's behavior for every command.

The contract

stdout is the command's primary result and nothing else. In text mode it is one canonical value (or one record per line, tab-separated for multi-column output). In --json mode it is a single JSON document.

stderr is every other byte the command emits: warnings, errors, progress indicators, status prose, prompts, banners, ABI dumps, verification chatter.

Corollaries

  • --json only changes stdout's format, never its channel cleanliness.
  • --quiet suppresses stderr diagnostics and progress. The target contract is that it never alters stdout content; today sh_print!/sh_println! are still suppressed by --quiet and that bypass will be flipped on once the major prose stdout call sites in forge/script have been migrated to sh_status!. sh_err! is the documented exception: fatal errors are always emitted on stderr.
  • -vvv adds stderr verbosity. It must never alter stdout content.
  • A command that has no primary result (e.g. forge install) writes nothing to stdout by default.
  • Prompts are diagnostics: write the question to stderr, read the answer from stdin.

The acceptance criterion this enables: an agent or shell script can run any command, discard stderr, and trust that stdout contains only the documented machine-readable result.

forge create 2>/dev/null              # → contract address only
forge test --json 2>/dev/null | jq    # → valid JSON, always
cast call 2>/dev/null | xargs       # → return value only

How to write code that follows the contract

Use the sh_* macros in [foundry_common::io]; do not call println! / eprintln! directly. A workspace-wide clippy disallowed-macros lint (see clippy.toml) forbids the four std::print* / std::eprint* macros everywhere; the sh_* macros expand to write! calls on the global Shell, so they are not affected by the lint.

MacroChannelSuppressed by --quietUse for
sh_println!stdoutyes (target: no, see above)The command's primary machine-readable result.
sh_print!stdoutyes (target: no, see above)Same as sh_println!, no trailing newline.
sh_status!stderryesStatus prose ("Compiling…", "Deploying contract…").
sh_progress!stderryes (also no-op when stderr ≠ tty)Spinner/progress-bar style transient updates.
sh_warn!stderryesRecoverable problems. Adds a "Warning:" prefix.
sh_err!stderrnoErrors. Adds an "Error:" prefix.
sh_eprintln!stderryesEscape hatch for raw stderr text that doesn't fit the above.
sh_eprint!stderryesSame as sh_eprintln!, no trailing newline.
prompt!stderr (question) + stdin (answer)yes (question is sh_eprint!)Interactive question/answer.

The compilation reporter in foundry_common::compile writes its spinner to stderr in TTY mode (see SpinnerReporter). The non-TTY fallback still writes to stdout today; flipping it to stderr will shift many existing snapshot tests and is part of the per-command migration backlog. Calling sh_progress! directly is also acceptable for one-off progress lines.

Decision rule for any sh_println! call site

For each sh_println! call you write or review, ask:

  1. Is this the canonical primary result of the command?
    • Yes → keep sh_println! (stdout).
    • No → use sh_status!, sh_warn!, sh_err!, or sh_eprintln! (stderr).
  2. Does this line mix labels with data (e.g. "Deployer: 0x…")?
    • The label is prose → move the whole line to sh_status! (stderr).
    • The data alone belongs on stdout → emit just the value (sh_println!).
    • In --json mode, both belong inside the single JSON document on stdout.

Per-command stdout contract (target)

This table is the target contract that migration PRs will bring each command into compliance with. It is the source of truth for what each command's stdout will contain after migration, not necessarily what it contains today. Each row's status is one of:

  • migrated — current behavior matches this contract.
  • todo — current behavior does not match yet; a follow-up PR is needed.

cast

CommandText mode stdout--json stdoutStatus
cast callReturn value (hex / decoded)JSON of return valuemigrated
cast sendReceipt (or tx hash with --async)JSON receipt (or hex tx hash with --async)migrated
cast estimateGas estimate (decimal)JSON { "gas": "…" }migrated
cast rpcRPC result (JSON)JSONmigrated
cast storageSingle slot valueJSON of layoutmigrated
cast logsPretty-printed logsJSON arraymigrated
cast runTrace / decoded outputJSONmigrated
cast traceTraceJSON tracemigrated
cast wallet newOne record per wallet: address (keystore) or address\tprivate_key (no keystore)JSON array of { address, public_key, path } (keystore) or { address, public_key, private_key } (no keystore)migrated
cast wallet signSignatureJSONmigrated
cast wallet sign-authSigned authorization RLPJSONmigrated
cast erc20 balanceBalance (decimal)JSON stringmigrated
cast create2address\tsalt (tab-separated)n/amigrated
cast access-listAccess listJSONmigrated
cast interfaceSolidity interface sourceJSON ABI arraymigrated
cast artifactJSON artifactn/amigrated
cast creation-codeHex bytecode (or disassembly with --disassemble)n/amigrated
cast constructor-argsOne constructor arg per linen/amigrated
cast b2e-payloadJSON execution payloadn/amigrated
cast tx-poolJSONJSONmigrated
cast da-estimateGas estimateJSONmigrated
cast find-blockBlock numberJSONmigrated
cast mktxSigned RLPJSONmigrated
cast batch-mktxSigned RLP (or unsigned RLP with --raw-unsigned)n/amigrated
cast batch-sendReceipt (or tx hash with --async)JSON receipt (or hex tx hash with --async)migrated

forge

CommandText mode stdout--json stdoutStatus
forge build(empty)JSON build outputtodo
forge test(empty; exit code = pass/fail)JSON test results, JUnit XML with --junittodo
forge createDeploy: Deployer: / Deployed to: / Transaction hash: lines. Dry-run: Contract: / Transaction: / ABI: lines. Compiler output may precede (tracked under forge build).JSON { deployer, deployedTo, transactionHash } (or dry-run JSON { contract, transaction, abi })todo
forge inspect <field>Just that field's value (artifact/output prints the contract artifact JSON)JSON of that fieldmigrated
forge install(empty)(empty)migrated
forge init(empty)(empty)migrated
forge update(empty)(empty)migrated
forge remove(empty)(empty)migrated
forge clone(empty)(empty)migrated
forge bind(empty)(empty)migrated
forge bind-json(empty) or generated pathJSONmigrated
forge flattenFlattened sourcen/amigrated
forge fmt(empty) or formatted source with --checkn/amigrated
forge treeDependency treeJSONmigrated
forge configConfig TOMLJSON configmigrated
forge selectorsSelectors outputJSONmigrated
forge eip712(empty)JSON of typesmigrated
forge geigerFindingsJSONmigrated
forge lint(empty; findings on stderr/exit code)JSON findingsmigrated
forge snapshotPer-test diff lines (--diff); table (--format table); (empty) otherwisen/amigrated
forge coverageCoverage table or reportJSON / LCOV / etc. via --reporttodo
forge cache(empty) or pathsJSONmigrated
forge clean(empty)n/amigrated
forge completionsGenerated shell completion scriptn/amigrated
forge doc(empty)n/amigrated
forge generate(empty) or generated pathn/amigrated
forge soldeerPassthrough to soldeer crate; foundry adds no wrapper prosen/amigrated
forge remappingsOne remapping per linen/amigrated
forge compilerCompiler infoJSONmigrated
forge verify-contract<guid-or-job-id>\t<url> on submission; empty if already verifiedn/amigrated
forge verify-bytecode<type> code matched with status <kind> linesJSON array of { bytecode_type, match_type, message }migrated

anvil, chisel, script

CommandText mode stdout--json stdoutStatus
anvilBanner, accounts, RPC URL on stderrn/atodo
chiselREPL outputn/atodo
forge scriptSimulation/broadcast resultJSONtodo

Commands not listed here have not been classified yet — please open an issue or PR before relying on their stdout format.

References