docs/dev/output-channels.md
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.
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.
--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.forge install) writes nothing to
stdout by default.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
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.
sh_println! call siteFor each sh_println! call you write or review, ask:
sh_println! (stdout).sh_status!, sh_warn!, sh_err!, or sh_eprintln! (stderr)."Deployer: 0x…")?
sh_status! (stderr).sh_println!).--json mode, both belong inside the single JSON document on stdout.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); omitted when stdout is a tty, where the stderr prose already shows the values | 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); when mining, omitted when stdout is a tty, where the stderr prose already shows the values | 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 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.
crates/common/src/io/macros.rscrates/common/src/io/shell.rscrates/common/src/term.rscrates/common/src/compile.rsclippy.toml