Apply systematic CLI design checks before writing or reviewing a command-line tool. Every rule is a concrete yes/no question — answer it, don't rationalize around it.
Signature Shape
- Flags over positional args. Max 2 positional args, and only when the action is primary and memorable (
cp <src> <dst>). Everything else gets--long-name. - Full names for every flag.
-halways has--help.-valways has--verbose. Short forms are for muscle-memory flags only. - No positional args with mixed meanings. If you have
cmd <file> <name>, add a third thing, or the second arg means two different things across subcommands — redesign to flags. - Order-independent.
myapp --verbose subcmdandmyapp subcmd --verbosemust both work.
Naming
- Reuse flag names across subcommands.
--jsonmeans JSON output everywhere.--repomeans a repository everywhere. Never use--jsonon one subcommand and--format jsonon another. - Follow the standard flag table.
-a/--all,-d/--debug,-f/--force,-h/--help,--json,--no-input,-o/--output,-p/--port,-q/--quiet,-u/--user,--version. Deviate only with a documented reason. - Verb consistency across resource subcommands. Use the same verb set everywhere:
list,get,create,update,delete. If one subcommand usesremoveand anotherdelete, pick one.
Help & Discovery
- Progressive disclosure, three levels.
cmd→ terse usage + one example.cmd subcmd→ subcommand-specific params.--help→ everything. Never dump full docs oncmdalone. - Lead with examples, not option catalogs. The first thing a user sees should be a working invocation they can copy.
- Suggest corrections on invalid input. If the user typed a command that doesn't exist but resembles one that does, say so. "Did you mean
ps?" not "unknown command."
Output
- stdout = data, stderr = messaging. Primary output, machine-readable output, and pipeable content go to stdout. Logs, errors, progress, and hints go to stderr.
--jsonfor structured output. When a machine might consume the output, provide--json. Pair with a filtering flag (--jqor--filter) to let callers trim before the data leaves the tool.--quietsuppresses non-essential output. Scripts should not redirect stderr to/dev/null— the tool should shut up when asked.- Default to human-readable, provide
--plainfor grep/awk. If terminal formatting (color, tables, dividers) would break line-based tooling,--plaindisables it.
Errors
- Every error says what happened AND what to do next. Not "permission denied" — "permission denied: cannot write to config.yaml. Run with
--config /path/to/writable/fileor usechmod +w config.yaml." - Never print a raw stack trace to users. Catch expected errors and rewrite them. Unexpected errors get a debug log file path plus a bug report URL.
Safety & Scriptability
- No secrets in flags.
--password,--token,--api-keyleak topsand shell history. Use--password-file, environment variables, or stdin. - Every prompt has a flag fallback. If the tool prompts for input interactively, provide
--name,--yes, or--confirmso scripts don't hang. - Confirm before destructive actions. Require
--forceor interactive confirmation for anything that can't be undone. For severe destruction (deleting a server), require typing the resource name. - Exit 0 on success, non-zero on failure. Map distinct non-zero codes to distinct failure modes so scripts can branch on them.
Function vs Form
- Make the default the right thing for 90% of users. If users need a flag for the common case, the default is wrong.
- If an action spans multiple underlying operations, make it one command.
mergeis one command, notcreate-merge-request+approve+apply. The user's intent is "merge this" — match it.