CLI Design
Programs are composable by default. stdout carries data, stderr carries diagnostics, exit codes carry status, and signals carry intent. Get the boundaries right and everything else follows.
References
- Arguments [
${CLAUDE_SKILL_DIR}/references/arguments.md] — Full POSIX guidelines, GNU long option table, subcommand patterns, flag design - Output [
${CLAUDE_SKILL_DIR}/references/output.md] — Stream separation details, color codes, ANSI escapes, NO_COLOR spec, pager setup - Exit codes [
${CLAUDE_SKILL_DIR}/references/exit-codes.md] — Standard/extended code tables, signal exit codes, partial success patterns - Interaction [
${CLAUDE_SKILL_DIR}/references/interaction.md] — TTY detection, prompting patterns, confirmation levels, progress display, error format - Configuration [
${CLAUDE_SKILL_DIR}/references/configuration.md] — Full hierarchy, XDG spec, env var catalog, config file formats, secret handling - Signals [
${CLAUDE_SKILL_DIR}/references/signals.md] — Full signal table, SIGPIPE handling, crash-only design, child process signals
Output Streams
This is the single most important convention. Mixing data and diagnostics in stdout is the #1 way to break composability.
- stdout is data. Primary output goes to stdout -- query results, computed values, formatted data. This is what gets piped to the next command or redirected to a file.
- stderr is diagnostics. Progress indicators, status messages, warnings, errors, and debug output go to stderr. Users see stderr in the terminal even when stdout is redirected.
- Check each stream independently. stdout may be piped while stderr is still a TTY. Adapt output format per-stream: human-readable when TTY, machine-parseable when piped.
- No animations when not a TTY. Disable spinners, progress bars, and color when the target stream is not an interactive terminal. This prevents CI logs from becoming escape-code garbage.
- Print something within 100ms. Before any network request or long operation, print a status line to stderr. Silence looks like a hang.
- Tell the user what changed. When a command modifies state, describe what happened and suggest what to do next.
Arguments
- POSIX short flags, GNU long flags. Single-letter options use
-x, multi-letter use--long-name. Every short flag must have a long equivalent. Reserve one-letter flags for frequently-used options. --terminates options. Everything after--is an operand, even if it starts with-. This is POSIX Guideline 10 and is non-negotiable.- Prefer flags over positional arguments. Flags are self-documenting. Exception: primary action on a single target
(
rm file.txt,cat file.txt). Two or more positional args for different things is a design smell. -means stdin/stdout. When a command accepts file arguments,-means read from stdin (or write to stdout when context is clear).- Use standard flag names.
-h/--help,--version,-v/--verbose,-q/--quiet,-f/--force,-n/--dry-run,-o/--output,--json,--no-color,--no-input. Don't reinvent these. - Make flags order-independent. Users add flags by pressing up-arrow and appending.
mycmd --flag subcmdandmycmd subcmd --flagshould both work where possible.
Subcommands
noun verborverb noun, pick one and be consistent. Don't mix ordering styles.docker container createanddocker image pullfollownoun verb.- Support help at every level.
mycmd --help,mycmd subcmd --help, andmycmd help subcmdshould all work. - No catch-all subcommand. Don't interpret unknown first arguments as an implicit default subcommand. This prevents you from ever adding new subcommands.
- No arbitrary abbreviations. If
mycmd ialiasesmycmd install, you can never addmycmd init. Aliases must be explicit and stable.
Exit Codes
- 0 = success, 1 = error, 2 = usage error. This covers most programs. Only define additional codes when callers need to distinguish specific failure modes.
- Signal exit codes use 128+N. SIGINT (Ctrl-C) exits 130, SIGTERM exits 143. Preserve this convention so calling scripts can distinguish "user cancelled" from "error."
- Never return 0 on failure. Scripts depend on exit codes via
$?andset -e. A false success silently breaks pipelines.
Configuration
- Flags > env vars > project config > user config > system config > defaults. This is the universal precedence hierarchy. A flag always wins over an env var, which always wins over a config file.
- Follow XDG for user-level files. Use
$XDG_CONFIG_HOME/myapp/(default~/.config/myapp/), not~/.myapp. RespectXDG_DATA_HOME,XDG_STATE_HOME,XDG_CACHE_HOME. - Respect well-known environment variables.
NO_COLOR,FORCE_COLOR,DEBUG,EDITOR,PAGER,HTTP_PROXY/HTTPS_PROXY,TMPDIR,HOME,TERM,LINES,COLUMNS. Check these before inventing app-specific alternatives. - Never read secrets from environment variables. They leak through
ps, Docker inspect, systemd, and process listings. Accept secrets via--password-file, stdin, or a credential helper. - Never read secrets from flags.
--password=secretleaks intopsoutput and shell history. Use--password-fileor stdin.
Color and Terminal Output
- Disable color when: stdout/stderr is not a TTY (check each independently),
NO_COLORis set and non-empty,TERM=dumb, or--no-coloris passed. - Support
FORCE_COLORor--colorto override detection and force color output when the user explicitly wants it. - Use color intentionally. Red for errors, green for success, yellow for warnings. Don't paint everything -- if everything is colored, nothing stands out.
- Use a color library. Don't hand-code ANSI escape sequences. Libraries handle NO_COLOR, TERM detection, and cross-platform differences.
Interactive Behavior
- Only prompt when stdin is a TTY. If stdin is not interactive, fail with an error telling the user which flag to pass. Never hang waiting for input that will never come.
- Provide
--no-input. An explicit flag to disable all prompts. Required for CI/CD, cron, and automation. - Confirm before destructive actions. Prompt for
y/Ninteractively, require--forcenon-interactively. For severe operations (deleting infrastructure), require typing the resource name. - Provide
--dry-run. For any state-modifying command, let users preview what would happen without executing it. - Don't echo passwords. Disable terminal echo when accepting secret input. Provide
--password-fileas a non-interactive alternative.
Signal Handling
- Respond to SIGINT immediately. Print "Shutting down..." before starting cleanup. Add a timeout so cleanup can't hang forever.
- Second Ctrl-C skips cleanup. Tell the user: "press Ctrl+C again to force." Then exit immediately.
- Handle SIGTERM like SIGINT. Process managers (systemd, Docker, K8s) send SIGTERM before SIGKILL. Complete cleanup within the grace period.
- Handle SIGPIPE silently. When the user pipes your output to
head, don't print an error when the pipe closes. Exit quietly with code 141 or 0. - Design for crash recovery. Use atomic file operations (write to temp, rename). Check for incomplete state on startup. Don't require cleanup to complete for correctness.
Help Text
- Support
-hand--help. Both must show help. Don't overload-hfor anything else. - Show concise help with no arguments. When a command requires arguments and gets none, show a brief description,
one or two examples, and a pointer to
--helpfor the full listing. - Lead with examples. Users read examples first. Show common invocations before the flag catalog.
- Suggest corrections. When the user types a close misspelling, suggest the correct command. Don't auto-execute the suggestion.
- Support
--version. Print the version and exit. Format: program name and version, optionally with build metadata.
Error Messages
- Structure errors consistently. Use a format like:
error: <what went wrong>followed byhint: <how to fix it>. - Be actionable. Don't say "permission denied" -- say "permission denied; run with sudo or use --config-dir for a writable location."
- Put important information last. The eye lands at the bottom of output. Put the error summary and fix there, not the stack trace.
- No stack traces by default. Put them behind
--verboseor write to a debug log file. Signal-to-noise ratio matters. - Make bug reporting easy. For unexpected errors, provide a URL or command to file a bug, pre-populated with diagnostic information.
Structured Output
- Support
--jsonfor machine-readable output. Output well-formed JSON to stdout. Use consistent field names across commands. - Support
--plainfor scriptable tabular output. One record per line, no color, no decorations. Useful forgrepandawkpipelines. - Use a pager for large output. Pipe through
less -FIRXwhen stdout is a TTY. Respect thePAGERenvironment variable. Don't page when output fits one screen.
Robustness
- Validate input early. Check arguments and flags before starting work. Report all validation errors at once, not one at a time.
- Make operations idempotent where possible. Running the same command twice should produce the same result, not an error or doubled state.
- Make operations recoverable. If the program fails partway through, the user can hit up-arrow and enter to retry from where it left off.
- Set network timeouts. Allow timeout configuration and have a reasonable default. Never hang indefinitely on a network call.
Naming
- Lowercase, short, memorable.
curlnotDownloadURL. Use only lowercase letters and hyphens. Keep it easy to type. - Avoid ambiguous subcommand names. Don't have both "update" and "upgrade". Use distinct verbs or disambiguate with extra words.
Future-Proofing
- Keep changes additive. Add new flags rather than changing existing behavior. If you must break compatibility, warn in advance.
- Warn before deprecation. When a user invokes a deprecated flag, tell them what to use instead and when the old form will be removed.
- Changing human-readable output is OK. Encourage
--jsonor--plainfor scripts. Human-readable output is not a stable interface.
Application
When writing CLI code:
- Apply all conventions silently. Don't narrate each rule.
- Use an argument parsing library (clap, cobra, argparse, commander).
- Wire up stdout/stderr correctly from the start. Retrofitting is painful.
- Handle SIGINT/SIGTERM from day one. Don't defer signal handling.
When reviewing CLI code:
- Check stdout vs stderr usage first. This is the most common mistake.
- Verify exit codes match actual success/failure.
- Confirm
--help,--version, and--no-colorare supported. - Look for secret leaks in flags and env vars.
Integration
This skill covers CLI platform concerns -- the interface between a program and the terminal/shell/pipeline. Language-specific implementation details (argument parsing libraries, terminal I/O APIs) come from language skills (golang, javascript, etc.). The coding discipline skill governs general workflow.
Programs are composable when they respect boundaries.