Good CLI Design Is Mostly Silence
· cli
A good CLI knows when to shut up. That sounds small, but it’s one of the clearest differences between a toy command and a tool people can trust every day.
This post goes through quiet mode, stdout and stderr, color, exit codes and hints, and why each needs a real contract.
A terminal command has two audiences. Sometimes a human is reading the output and sometimes a script is, and the script has no sense of humor. A person can ignore a progress line but a script can’t. A person can understand that Done! is not part of the JSON, while a script writes it to a file and breaks the next command.
Output is not decoration. Output is an interface.
Quiet Means Quiet
--quiet should mean something strong.
It should not mean “print fewer messages.” It should not mean “hide the banner but keep the tips.” It should not mean “be quiet except during setup, because setup is special.” That’s how tools become annoying.
Quiet means that if the command succeeds and there is nothing important to say, it prints nothing at all. Bad quiet mode looks like this:
Initializing...
Done!
Tip: run tool status nextFine for a demo. Bad for automation.
If a user asks for quiet output, they probably want to compose your command with something else. They may redirect stdout or compare output or run it in CI (don’t make them clean up your friendly messages).
stdout Is for Data
stdout and stderr are different tools. stdout is for the result the user asked for, and stderr is for logs, warnings, progress and diagnostics. Say I run this command to save a list of items as JSON:
tool list --json > items.jsonThe file items.json should then contain JSON, and not JSON plus Fetching items..., or JSON plus a warning, or JSON plus a version update notice.
Just JSON.
This rule sounds strict because it needs to be strict. The moment diagnostics leak into stdout, the command gets worse in pipes and in scripts and in CI. A CLI that mixes data and logs isn’t friendly, it is messy.
Color Must Be Controlled
Color helps humans because it makes errors easier to see and status output easier to scan. But color should never surprise the caller, so a good CLI needs a way to control it with flags like these (or something equivalent):
--color=auto
--color=always
--color=neverAuto is the right default, which means you use color when the stream you write to is a terminal and you don’t use color when it is redirected. Auto should also turn color off when NO_COLOR is set to a non-empty value.
ANSI codes in logs are annoying, and ANSI codes in JSON are worse because they’re small invisible bugs that travel everywhere (files, CI logs and copied error reports).
Color is a feature. Keep it in its lane.
Exit Codes Are Output Too
Exit codes are part of the API, and a command can print perfect text and still be broken if the exit code lies. A diff command can use exit codes like this:
0means there is no diff.1means the command found a diff.2means the command itself failed.
That makes it useful in scripts:
tool diff --quiet
case $? in
0) echo "clean" ;;
1) echo "changed" ;;
*) echo "diff failed" >&2; exit 1 ;;
esacThe case branches on the exit code of tool diff --quiet alone, so a failure never passes for a diff. No parsing. No grep. No guessing.
That’s the point, because a script should not need to read English sentences to understand what happened.
Human Output and Machine Output Are Different
Human output should be readable:
3 files changed
modified ~/.config/app/config.yaml
missing ~/.ssh/example_key
stale ~/bin/helper
Run `tool apply` to restore them.Machine output should be stable, with the same fields every time:
{
"changed": true,
"items": [
{ "path": "~/.config/app/config.yaml", "status": "modified" },
{ "path": "~/.ssh/example_key", "status": "missing" },
{ "path": "~/bin/helper", "status": "stale" }
]
}Don’t try to make one format serve both jobs. Text output can improve over time, but JSON output is a contract, and if people build scripts on it then breaking it is a real breaking change.
Good Hints Are Short and Optional
A CLI should help users find the next command, but it shouldn’t turn every success message into a tutorial. This is too much for a command that just succeeded:
Success! Your project is initialized!
Here are 12 commands you can run next...This is better and still points at the next step:
Initialized ~/.config/example
Next:
tool status
tool applyShort. Useful. Easy to ignore.
And of course there are no hints in quiet mode, no hints in JSON, and no hints in stdout when stdout is data. The same rule keeps showing up, which is to be helpful but respect the contract.
Silence Is Part of the Design
Good CLI design isn’t just commands and flags. It is trust, and it comes down to a few questions. Can I pipe this into another command, redirect it to a file, use the exit code in CI, turn color off, and ask it to be quiet and believe it? If the answer is yes, the tool feels solid.
The best CLI output is often the output you do not print, and not because minimalism is cool but because silence gives control back to the caller.