ax-check rule
AXC-C003: Error text not on stderr
On an error, the message went to stdout and stderr was empty, so it mixes with output a script or agent parses.
ax-check is a checker being prepared for release. This page documents the rule ahead of that release; see all 50 rules.
| Severity | warn |
| Kind | Observed. Depends on the program and environment at run time. |
| Mode | ax-check probe-cli |
| Applies to | Command-line programs, run as a subprocess |
| Pattern tags | exit-codes, error-recovery |
| Fix in one line | Write errors and diagnostics to stderr; keep stdout for the result. |
On an error, the message went to stdout and stderr stayed empty. Scripts and agents treat stdout as the result. An error mixed into it can be parsed as data.
What it checks
The rule looks at the unknown-subcommand, unknown-flag and missing-value probes, which run only with --probe errors. It fires for a probe that exits non-zero, wrote nothing to stderr and wrote something to stdout. The unknown-subcommand probe only counts when the help output lists subcommands, and the missing-value probe only runs when help documents a flag that takes a value.
Why it matters
Programs that call your CLI usually capture stdout as the result and show or log stderr. If an error lands on stdout, a pipeline such as invoicer list --json | jq . feeds the error text into the next step. The Command Line Interface Guidelines say: “Send messaging to stderr. Log messages, errors, and so on should all be sent to stderr.” Cloudflare’s documentation for its cf CLI describes the same contract: the result goes to stdout as JSON, and messages and errors go to stderr (vendor documentation).
How to fix
Write errors, warnings and progress messages to stderr. Keep stdout for the result only. Most argument parsers have a setting for this, so check how your framework prints usage errors.
Example
Before
$ invoicer --axcheck-unknown-flag </dev/null 1>out.txt 2>err.txt; echo $?
2
$ cat out.txt
error: unknown flag --axcheck-unknown-flag
$ wc -c err.txt
0 err.txt
After
$ invoicer --axcheck-unknown-flag </dev/null 1>out.txt 2>err.txt; echo $?
2
$ wc -c out.txt
0 out.txt
$ cat err.txt
error: unknown flag --axcheck-unknown-flag
Run: invoicer --help
A Python fix:
print("error: unknown flag --axcheck-unknown-flag", file=sys.stderr)
sys.exit(2)
How ax-check detects it
The probe runs with separate pipes for stdout and stderr, so ax-check can tell which stream carried the text. The rule needs three things at once: a non-zero exit, empty stderr and non-empty stdout. If the exit status is 0, the problem is reported by AXC-C002 instead.
To reproduce by hand: invoicer --axcheck-unknown-flag </dev/null 1>out.txt 2>err.txt; wc -c out.txt err.txt.
Known false negatives: a CLI that writes the error to both streams passes. Known false positives: a tool that deliberately prints its usage text to stdout on error, which some older conventions allow. Silence the rule with --disable AXC-C003 when this is intended.
Safety note: The error probes this rule needs are opt-in: run with --probe errors. Without it the rule is not checked, and the report lists it as not checked. Argument parsers normally reject an unknown subcommand, an unknown flag or a missing value before doing any work, but a program that does not validate its arguments will do its ordinary work instead. probe-cli executes your program under your own OS account. The temporary working directory and HOME it uses are not a sandbox: the program can still read and write anything your account can, and use the network. Probe a CLI you have not reviewed only inside a container or a throwaway virtual machine. Run ax-check probe-cli --dry-run -- invoicer first to see the probes that would run, and see the “Probe safety” section of the ax-check README for what is and is not isolated.
Sources
- Guideline: Command Line Interface Guidelines, clig.dev. https://clig.dev/ . Says to send messaging, including log messages and errors, to stderr.
- Vendor documentation: Use cf with coding agents, Cloudflare, updated 2026-09-29. https://developers.cloudflare.com/cf/agents/ . Describes an output contract with the result on stdout as JSON, and messages and errors on stderr.
Related evidence
Records in the AX evidence register that share a pattern tag with this rule. A shared tag means the record is about the same pattern, not that it tests this rule. Read the evidence class before the number.
- EV-0002: Constraints stated only in prose produce silent failures on live APIs (Preprint). SilentProbe (preprint) reports that only 7.5% of 2,501 public OpenAPI documents declare an enum, and that on live endpoints machine-checkable constraints returned an honest error in 111 of 111 cases while prose-only constraints failed silently in 44 of 61.
- EV-0003: Error text that names the next tool lifts recovery (Preprint). A preprint testing five OpenAI models reports that an expired-credential error naming a terminal command left 45% of tasks recovered, naming the server's login tool instead raised recovery to 84%, and on rate limits naming the call to repeat raised recovery from 6% to 88%.
- EV-0004: Naming recovery tools is the active ingredient in failure receipts (Preprint). Outcome Monitors (preprint) reports that receipts naming a violated outcome and the public recovery tools raised ToolMaze completion from 10.9% to 28.1% across four models, and that removing the list of recovery tools eliminated the gain.
- EV-0005: Idempotency keys cut duplicate writes from 28% to 4% (Preprint). LIMBO (preprint) reports that offering an idempotency key on every write cut duplicate side effects from 28% to 4% of episodes because agents use keys when they exist, and that agents reported success in 90% of the episodes in which they had duplicated an effect.
- EV-0036: A CLI exits 0 when a destructive command is refused (Independent measurement). Cloudflare's cf CLI documents that in a non-interactive session a destructive command without --force prints 'Aborted.' and exits with status 0, and a public issue reproduces this for workflows delete.
- EV-0037: Destructive and bulk commands that report success but do nothing (Independent measurement). Users of Cloudflare's cf beta report commands that exit 0 without doing the work: R2 deletes of keys containing '/' (a cleanup script reported 2,220 objects deleted that were all still present) and a secrets bulk update that deployed an empty change to 100%.
