ax-check rule
AXC-C010: ANSI colour codes without a terminal
Output contained ANSI escape sequences although stdout and stderr were not terminals.
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 | non-interactive, context-budget |
| Fix in one line | Only colour output when the stream is a TTY; check each stream separately. |
The output contained ANSI escape sequences even though stdout and stderr were pipes, not terminals. Those sequences are invisible on a screen but show up as noise when a program or an agent reads the raw bytes.
What it checks
The rule scans the output of every probe that ran, except the no-color probe, for ANSI escape sequences, such as ESC[31m for colour or ESC[2K for erasing a line. It produces one finding, which lists every probe that emitted one. Every probe runs with no TTY, with CI=1 set, and with FORCE_COLOR, CLICOLOR_FORCE and NO_COLOR removed from the environment. This means ax-check has not asked for colour in any way.
Why it matters
Raw escape codes break string matching, add characters to every line and make parsed output harder to read. An agent that reads \x1b[31merror\x1b[0m must work out that the extra characters are decoration. They also take space in the model context for no gain. The Command Line Interface Guidelines say to disable colour when “stdout or stderr is not an interactive terminal (a TTY)”, when NO_COLOR is set, when TERM is dumb, or with --no-color.
How to fix
Colour only when the stream is a TTY, and check stdout and stderr separately, since one can be a terminal while the other is a pipe. Most colour libraries do this for you, but only if you ask them to detect the stream and not force colour on.
Example
Before
$ invoicer list --status bogus </dev/null 2>&1 | cat -v
^[[31merror^[[0m: unknown status "bogus"
^[[2mValid values: open, paid, void^[[0m
After
$ invoicer list --status bogus </dev/null 2>&1 | cat -v
error: unknown status "bogus"
Valid values: open, paid, void
A Node fix:
const useColour = process.stderr.isTTY && !process.env.NO_COLOR && process.env.TERM !== 'dumb';
const red = (s) => (useColour ? `\x1b[31m${s}\x1b[0m` : s);
console.error(`${red('error')}: unknown status "bogus"`);
A Python fix:
use_colour = sys.stderr.isatty() and not os.environ.get("NO_COLOR") and os.environ.get("TERM") != "dumb"
How ax-check detects it
ax-check searches the captured stdout and stderr of each probe for any ANSI escape sequence: ESC [, then parameters, then a final character. It does not decode what the sequence does, so cursor movement and line erasing count as well as colour. That is deliberate: none of these belong in piped output. AXC-C011 is narrower and checks colour codes only.
The no-color probe is not scanned here, because AXC-C011 covers it. If your program colours only when FORCE_COLOR is set, it passes here because ax-check removes that variable.
To reproduce by hand: invoicer --help </dev/null 2>&1 | cat -v | grep -n '\^\[\['.
Known false positives: tools whose purpose is to produce coloured output for later display. Silence the rule with --disable AXC-C010 for those.
Safety note: This rule scans every probe that ran; by default, only the help and version probes. 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/ . Lists the cases where colour should be disabled: stdout or stderr is not a TTY,
NO_COLORis set,TERMisdumb, or--no-coloris passed. - Specification: NO_COLOR, no-color.org. https://no-color.org/ . Says software that adds ANSI colour by default should check for
NO_COLOR.
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-0017: Injected skills lowered pass rates and raised token cost on average (Preprint). WebDev-Skills-Bench (preprint) found that injecting matched public skills reduced mean Pass@2 by 1.3% to 4.2% across four models and raised token cost by 72% to 394%, with gains in only 17% to 36% of skill-project pairs.
- EV-0022: Search-and-execute meta-tools cut catalogue tokens by 99% in production (Preprint). PayPal authors report (preprint) that exposing two meta-tools, search and execute, over 2,000+ MCP tools cut tool-token consumption in production from 140.2k tokens (70.1% of context) to 1.3k tokens (0.8%).
- EV-0027: A capable agent skipped the index and guessed the page (Preprint). A preregistered ablation on a 709-page Markdown wiki (preprint) found that a capable tool-using agent never loaded the compact catalogue index, inferring page paths from the question instead, while retrieval-based access kept answer quality non-inferior and cut cost by about a third to over half.
- 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-0039: Launch post and documentation disagree on agent output format (Independent measurement). Cloudflare's cf launch post says JSON output is 'condensed for agents', but the cf documentation says JSON output is indented whether or not output is a terminal, and a public issue reports byte-identical output with an agent detected.
