All 24 checks with why-it-matters prose, severity, and cross-references to related audits.
A CLI without help text is a CLI no one can use. Users land on your tool via `npx mytool` or `pip install`, type `--help` out of reflex, and either see nothing or see a framework-default skeleton. They close the terminal and find a competitor. Missing descriptions also break shell autocomplete hints, man-page generation, and docs scrapers that harvest `--help` output. This is a user-experience failure severe enough to zero out adoption regardless of how good the underlying logic is.
Why this severity: Critical because without `--help` output the CLI is effectively undiscoverable and unusable for any new user.
cli-quality.command-structure.help-flagSee full patternAutomation scripts, CI pipelines, and package managers all query `--version` to verify which release is installed before running. Without a working version flag, a script cannot confirm it's using the correct build, which leads to silent version mismatches in production. A hardcoded version string that drifts from `package.json` (or `pyproject.toml` / `Cargo.toml`) compounds the problem: tools report a version they are not. ISO 25010:2011 usability.learnability directly penalizes discoverable interfaces that omit this basic contract.
Why this severity: High because a missing or stale `--version` flag breaks automated upgrade checks and CI gating, causing silent version drift in production.
cli-quality.command-structure.version-flagSee full patternInconsistent subcommand naming destroys discoverability. When a user learns `user:create` exists, they expect `team:create` to follow the same pattern — if instead they find `createTeam` or `team_create`, they have to read the full help text for every resource. Mixed conventions (kebab-case for some commands, camelCase for others, snake_case for a third) signal an AI-assembled codebase where different sessions added commands without checking what already existed. Beyond user friction, ISO 25010:2011 maintainability.consistency flags this directly: inconsistent naming makes the surface area harder to test, document, and extend.
Why this severity: High because inconsistent naming breaks user mental models and makes every new command unpredictable, which directly degrades ISO 25010:2011 usability.learnability.
cli-quality.command-structure.subcommand-patternSee full patternUsers have thirty years of muscle memory around POSIX flags. When `-verbose` works but `--verbose` does not, or when `--port=3000` silently parses as `--port` with a positional `=3000`, users assume the tool is broken and stop typing. Non-standard flag syntax also breaks shell completions, wrapper scripts, and Makefile recipes that assume GNU conventions. A tool that invents its own flag grammar is a tool that will never be piped into anything.
Why this severity: Medium because broken POSIX conventions frustrate every user but rarely cause silent data corruption.
cli-quality.command-structure.posix-flagsSee full patternHidden commands are where CLI trust goes to die. When README documents a feature that does not appear in `--help`, or when `mytool` shows four commands but actually ships seven, users stop believing the help output is authoritative. They start grepping source or running `strings` on the binary. Commands registered without descriptions also silently vanish from generated docs and shell completions, leaving users to discover functionality through support tickets and tribal knowledge.
Why this severity: Low because hidden commands cause discoverability friction but rarely break working integrations.
cli-quality.command-structure.no-hidden-commandsSee full pattern`deploy target env` tells the user nothing about which argument is required, which is optional, or what the default is. They guess, hit an error, guess again, and eventually give up. Unclear argument signatures also break automated documentation and API-shaped wrappers that parse help output. When a missing required argument triggers a stack trace instead of `Error: missing argument <target>`, users interpret the tool as unstable and stop using it in CI.
Why this severity: Medium because unclear arguments produce constant user friction and support load but rarely break production.
cli-quality.command-structure.arg-claritySee full patternWhen a CLI mixes data output and status messages on stdout, any pipeline that consumes the output breaks silently. Running `mytool list-users | jq '.[].email'` will fail or produce corrupt results if progress text like "Fetching 42 users..." lands in the same stream as the JSON. Docker, `xargs`, `tee`, and CI log parsers all assume stdout carries only data. Sending error messages to stdout also defeats shell `&&` semantics: a caller checking exit code sees 0 but its stdin feed contains garbage. This is classified as a critical reliability failure under ISO 25010:2011 usability.operability because the entire composability contract of the Unix tool model breaks at this seam.
Why this severity: Critical because stdout/stderr mixing silently corrupts downstream pipeline consumers and CI log parsers — fixing exit codes alone does not restore composability.
cli-quality.io-behavior.stdout-stderr-separationSee full patternA CLI without `--json` output is a CLI that cannot be scripted. Users pipe your tool into `jq`, feed it into CI, wrap it in Makefile recipes, and compose it with other tools — and all of that requires structured, decoration-free output on stdout. When `mytool list` prints a pretty table with emoji dividers, the user has to write a regex parser in bash to extract one field. They will not. They will switch tools. Mixing status messages into JSON output (`Loading...\n{...}`) is worse — it silently breaks `| jq` without any error.
Why this severity: High because missing machine-readable output blocks CI, automation, and composition with Unix pipelines.
cli-quality.io-behavior.machine-outputSee full patternUnix philosophy is composition, and composition requires stdin. When `mytool validate` only accepts `--file path.json` and refuses piped input, users cannot run `curl api.example.com/data | mytool validate` or chain your tool into generators. Tools that cannot participate in pipelines get replaced by tools that can. Worse, stdin reading without TTY detection causes interactive invocations to hang forever — the user sees a frozen terminal and assumes the tool crashed.
Why this severity: Medium because missing stdin support blocks pipeline composition without causing data loss or crashes.
cli-quality.io-behavior.stdin-pipeSee full patternWhen `mytool list > out.txt` writes `\x1b[32mOK\x1b[0m` into the file, the user opens `out.txt` and sees escape-code garbage. When CI logs capture ANSI codes as `^[[32m`, grep patterns break. When a blind user's screen reader hits color codes, output becomes unintelligible. The `NO_COLOR` convention (no-color.org) and TTY detection exist for exactly this reason — ignoring them corrupts piped output, log files, and accessibility tooling.
Why this severity: Medium because raw ANSI codes in piped output or log files corrupt downstream consumers and break accessibility.
cli-quality.io-behavior.no-color-supportSee full patternA CLI that sits silent for ten seconds during a deploy looks frozen. Users hit Ctrl+C, corrupt state, and open a support ticket. Progress indicators on stderr — spinners, counters, bars — tell the user the tool is alive without polluting stdout for scripts. Writing spinners to stdout is the opposite failure: `mytool deploy | tee log` captures terminal control sequences into the log file, and every subsequent `cat log` flickers the terminal.
Why this severity: Low because missing progress indication is a UX annoyance but does not cause data loss or failed operations.
cli-quality.io-behavior.progress-indicationSee full patternCI pipelines need quiet mode — `mytool build --quiet` so the log does not balloon with per-file output. Debugging sessions need verbose mode — `mytool build -v` to see which request failed or which config was loaded. A CLI that only outputs one noise level forces users into either information overload in CI or flying blind during incidents. Registering `--verbose` but never reading it in handlers is a worse sin: the flag looks supported, users trust it, and it silently does nothing.
Why this severity: Low because missing verbosity flags degrade debugging and CI output but do not affect core functionality.
cli-quality.io-behavior.verbosity-levelsSee full patternShell pipelines, `&&` chains, `make` targets, and CI systems all read exit codes to decide whether to continue. A CLI that exits 0 after printing an error message signals success to every caller — the deploy runs, the migration fires, the release ships. CWE-390 (Detection of Error Condition Without Action) covers exactly this failure: the error is detected and logged, but the process status communicates success. AI-generated CLIs frequently introduce this bug by placing `process.exit(0)` (or nothing at all) in catch blocks. ISO 25010:2011 reliability.fault-tolerance requires that faults be observable by callers — exit code 0 makes them invisible.
Why this severity: Critical because a non-zero exit code is the only signal CI pipelines and shell scripts have that a command failed — exit 0 on error makes failures invisible to automation.
cli-quality.error-handling.exit-codesSee full pattern`Error: ENOENT: no such file or directory, open '/home/user/.mytoolrc'` tells the user nothing they can act on. They Google the error code, land on Stack Overflow, waste thirty minutes. `Configuration file not found: ~/.mytoolrc. Run \`mytool init\` to create one.` solves the problem in one line. Raw exception messages are a signal the tool was built without thinking about the user's experience on the failure path — which is, for most users, the majority of their interaction with the tool.
Why this severity: High because unhelpful errors drive support load, abandonment, and block every downstream task until the user figures it out.
cli-quality.error-handling.error-messagesSee full patternWhen a required argument is silently missing, the command either crashes mid-execution with a raw `TypeError: Cannot read properties of undefined` — dumping a stack trace onto the user — or it runs with `undefined` as a value and produces silent corruption downstream (a deploy target of `undefined`, a file named `undefined.json`, a database query with a null key). CWE-233 (Improper Handling of Parameters) applies: the input is invalid, but no validation gate catches it before execution begins. ISO 25010:2011 usability.learnability penalizes CLIs that make users guess what went wrong — the error message must name the missing argument and show the correct usage.
Why this severity: High because missing required arguments that produce TypeErrors or silent `undefined` behavior give users no actionable path to recovery without reading source code.
cli-quality.error-handling.missing-arg-handlingSee full patternWhen a CLI silently ignores unknown flags (via `.allowUnknownOption()` or equivalent), user typos produce no error — `--foramt json` is swallowed, the command runs with the wrong output format, and the user thinks it succeeded. CWE-20 (Improper Input Validation) covers this precisely: inputs that fall outside the accepted set must be rejected, not ignored. The same principle applies to typed arguments: a `--count abc` that coerces to `NaN` and propagates through downstream logic produces silent corruption that can be harder to diagnose than a stack trace. Strict input rejection at the boundary turns typos into immediate feedback instead of silent misconfigurations.
Why this severity: Medium because silent acceptance of invalid inputs produces wrong results without any error signal, which is harder to diagnose than a crash.
cli-quality.error-handling.invalid-inputSee full patternWhen a user presses Ctrl+C mid-operation and the CLI has no SIGINT handler, any temporary files written during that run become orphaned, any terminal modifications (hidden cursor, raw mode, alternate screen) persist into the next shell prompt, and partial output files are left in a state that corrupts subsequent runs. ISO 25010:2011 reliability.fault-tolerance requires that interruption be a recoverable event, not a resource leak. The exit code matters too: a process killed by SIGINT that exits 0 misleads shell scripts into treating the interrupt as a successful completion — the conventional exit codes are 130 (SIGINT) and 143 (SIGTERM), encoding `128 + signal_number`.
Why this severity: Medium because absent signal handling leaves orphaned resources and corrupted terminal state on every Ctrl+C, but the impact is bounded to the local user session.
cli-quality.error-handling.signal-handlingSee full patternA raw `ENOENT: no such file or directory, open 'path/to/file'` stack trace tells the user the OS error code and the internal call stack — neither of which tells them what they should actually do. CWE-391 (Unchecked Error Condition) applies: the filesystem error is surfaced but not handled into a user-actionable message. When a CLI accepts user-specified file paths as arguments, it must own the responsibility of validating those paths before processing begins. Failing mid-processing — after partial output has already been written — is worse than failing fast at the boundary.
Why this severity: Low because the error is immediately visible to the user and does not propagate silently — it degrades UX but does not cause data corruption or security exposure.
cli-quality.error-handling.file-not-foundSee full patternA `bin` field pointing to `./dist/cli.js` when no `dist/` directory exists is a package that installs successfully and then fails on first invocation with `env: node: No such file or directory` or `Cannot find module`. Pointing `bin` at a `.ts` file is worse — `npx mytool` dies with a syntax error on the first `import` statement. Users uninstall immediately and leave a one-star review. This is the single most common CLI publish-to-npm failure mode.
Why this severity: Critical because a broken entry point makes the CLI uninstallable or non-executable on every user's first try.
cli-quality.config-distribution.bin-fieldSee full patternWithout `#!/usr/bin/env node` on the first line of `dist/cli.js`, Linux and macOS refuse to execute the file — users see `cannot execute binary file: Exec format error` and have no way to fix it short of prefixing `node` manually. Hardcoding `#!/usr/local/bin/node` is nearly as bad: it breaks on systems using nvm, asdf, volta, Homebrew's Apple Silicon paths, or any container where node lives elsewhere. `env`-based shebangs are the only portable form.
Why this severity: High because a missing or hardcoded shebang breaks execution on Unix systems for a large fraction of users.
cli-quality.config-distribution.shebangSee full patternWithout a config file, users must pass `--api-key`, `--format`, `--output-dir`, and `--region` on every single invocation. That friction drives them toward shell aliases that are fragile, `.env` files that aren't portable, and wrapper scripts that become undocumented infrastructure. ISO 25010:2011 usability.operability scores CLIs on whether persistent preferences can be set once and forgotten. A CLI that requires 6 flags per invocation is not operationally usable in real workflows — teams will script around it rather than into it, breaking the tool's intended composability.
Why this severity: Medium because the absence of config file support imposes per-invocation repetition that degrades usability in team workflows without causing data loss or security failures.
cli-quality.config-distribution.config-fileSee full patternAPI keys and tokens passed as command-line flags appear in shell history (`~/.bash_history`, `~/.zsh_history`), in `ps aux` output visible to other users on the same machine, and in CI log output if the command is echoed before execution. CWE-214 (Sensitive Information in Command Line Arguments) documents this exact exposure vector. A contractor running `mytool --api-key sk-live-abc123 deploy` on a shared build server has just written that key into a file that persists after logout. Environment variables avoid all three exposure paths: they are not logged by the shell history, not visible in `ps` argument lists, and not echoed in CI unless explicitly printed.
Why this severity: Low because the exposure requires access to the user's shell history or process list — not network-accessible — but live API key leakage via `ps` on shared servers is a real, documented attack.
cli-quality.config-distribution.env-varsSee full patternShell completions turn `mytool dep<TAB>` into `mytool deploy` and `mytool deploy --en<TAB>` into `mytool deploy --environment`. Power users depend on them; new users discover commands through them. A CLI with twelve subcommands and no completion support forces users to consult `--help` for every invocation. Completion scripts are cheap to generate (one line in cobra, one crate in clap, built into oclif) and disproportionately improve daily use.
Why this severity: Info because completions are a usability enhancement, not a correctness or availability concern.
cli-quality.config-distribution.shell-completionsSee full patternIf `npx my-cli-tool` fails, ninety percent of prospective users never try the tool again. Missing `files` causes npm to publish test fixtures, node_modules snapshots, and local `.env` files — leaking secrets and bloating the package from 50KB to 50MB. Missing `engines` means a user on Node 14 runs `npx mytool` and sees a cryptic `SyntaxError: Unexpected token '?'` from optional chaining in your ES2020 code. Missing `bin` means there's no command to run at all.
Why this severity: High because distribution config errors block global installation and can leak secrets or fail on common runtime versions.
cli-quality.config-distribution.npx-installSee full patternRun this audit in your AI coding tool (Claude Code, Cursor, Bolt, etc.) and submit results here for scoring and benchmarks.
Open CLI Quality Audit