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
--jsonmode 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
--jsononly changes stdout's format, never its channel cleanliness.--quietsuppresses stderr diagnostics and progress. The target contract is that it never alters stdout content; todaysh_print!/sh_println!are still suppressed by--quietand that bypass will be flipped on once the major prose stdout call sites inforge/scripthave been migrated tosh_status!.sh_err!is the documented exception: fatal errors are always emitted on stderr.-vvvadds 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.
| Macro | Channel | Suppressed by --quiet | Use for |
|---|---|---|---|
sh_println! | stdout | yes (target: no, see above) | The command's primary machine-readable result. |
sh_print! | stdout | yes (target: no, see above) | Same as sh_println!, no trailing newline. |
sh_status! | stderr | yes | Status prose ("Compiling…", "Deploying contract…"). |
sh_progress! | stderr | yes (also no-op when stderr ≠ tty) | Spinner/progress-bar style transient updates. |
sh_warn! | stderr | yes | Recoverable problems. Adds a "Warning:" prefix. |
sh_err! | stderr | no | Errors. Adds an "Error:" prefix. |
sh_eprintln! | stderr | yes | Escape hatch for raw stderr text that doesn't fit the above. |
sh_eprint! | stderr | yes | Same 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:
- Is this the canonical primary result of the command?
- Yes → keep
sh_println!(stdout). - No → use
sh_status!,sh_warn!,sh_err!, orsh_eprintln!(stderr).
- Yes → keep
- 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
--jsonmode, both belong inside the single JSON document on stdout.
- The label is prose → move the whole line to
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
| Command | Text mode stdout | --json stdout | Status |
|---|---|---|---|
cast call | Return value (hex / decoded) | JSON of return value | migrated |
cast send | Receipt (or tx hash with --async) | JSON receipt (or hex tx hash with --async) | migrated |
cast estimate | Gas estimate (decimal) | JSON { "gas": "…" } | migrated |
cast rpc | RPC result (JSON) | JSON | migrated |
cast storage | Single slot value | JSON of layout | migrated |
cast logs | Pretty-printed logs | JSON array | migrated |
cast run | Trace / decoded output | JSON | migrated |
cast trace | Trace | JSON trace | migrated |
cast wallet new | One 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 sign | Signature | JSON | migrated |
cast wallet sign-auth | Signed authorization RLP | JSON | migrated |
cast erc20 balance | Balance (decimal) | JSON string | migrated |
cast create2 | address\tsalt (tab-separated) | n/a | migrated |
cast access-list | Access list | JSON | migrated |
cast interface | Solidity interface source | JSON ABI array | migrated |
cast artifact | JSON artifact | n/a | migrated |
cast creation-code | Hex bytecode (or disassembly with --disassemble) | n/a | migrated |
cast constructor-args | One constructor arg per line | n/a | migrated |
cast b2e-payload | JSON execution payload | n/a | migrated |
cast tx-pool | JSON | JSON | migrated |
cast da-estimate | Gas estimate | JSON | migrated |
cast find-block | Block number | JSON | migrated |
cast mktx | Signed RLP | JSON | migrated |
cast batch-mktx | Signed RLP (or unsigned RLP with --raw-unsigned) | n/a | migrated |
cast batch-send | Receipt (or tx hash with --async) | JSON receipt (or hex tx hash with --async) | migrated |
forge
| Command | Text mode stdout | --json stdout | Status |
|---|---|---|---|
forge build | (empty) | JSON build output | todo |
forge test | (empty; exit code = pass/fail) | JSON test results, JUnit XML with --junit | todo |
forge create | Deploy: 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 field | migrated |
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 path | JSON | migrated |
forge flatten | Flattened source | n/a | migrated |
forge fmt | (empty) or formatted source with --check | n/a | migrated |
forge tree | Dependency tree | JSON | migrated |
forge config | Config TOML | JSON config | migrated |
forge selectors | Selectors output | JSON | migrated |
forge eip712 | (empty) | JSON of types | migrated |
forge geiger | Findings | JSON | migrated |
forge lint | (empty; findings on stderr/exit code) | JSON findings | migrated |
forge snapshot | Per-test diff lines (--diff); table (--format table); (empty) otherwise | n/a | migrated |
forge coverage | Coverage table or report | JSON / LCOV / etc. via --report | todo |
forge cache | (empty) or paths | JSON | migrated |
forge clean | (empty) | n/a | migrated |
forge completions | Generated shell completion script | n/a | migrated |
forge doc | (empty) | n/a | migrated |
forge generate | (empty) or generated path | n/a | migrated |
forge soldeer | Passthrough to soldeer crate; foundry adds no wrapper prose | n/a | migrated |
forge remappings | One remapping per line | n/a | migrated |
forge compiler | Compiler info | JSON | migrated |
forge verify-contract | <guid-or-job-id>\t<url> on submission; empty if already verified | n/a | migrated |
forge verify-bytecode | <type> code matched with status <kind> lines | JSON array of { bytecode_type, match_type, message } | migrated |
anvil, chisel, script
| Command | Text mode stdout | --json stdout | Status |
|---|---|---|---|
anvil | Banner, accounts, RPC URL on stderr | n/a | todo |
chisel | REPL output | n/a | todo |
forge script | Simulation/broadcast result | JSON | todo |
Commands not listed here have not been classified yet — please open an issue or PR before relying on their stdout format.
References
- Macros:
crates/common/src/io/macros.rs - Shell wrapper:
crates/common/src/io/shell.rs - Spinner / progress:
crates/common/src/term.rs - Compilation reporter:
crates/common/src/compile.rs - Lint configuration:
clippy.toml