P6: Composable and Predictable Command Structure
Definition
CLI tools MUST integrate cleanly with pipes, scripts, and other tools. That means handling SIGPIPE, detecting TTY for color and formatting decisions, supporting stdin for piped input, and maintaining a consistent, predictable subcommand structure.
Why Agents Need It
Agents compose CLI tools into pipelines:
tool list --output json | jaq '.[] | .id' | xargs tool get
Every link in that chain has to behave predictably. A tool that panics on SIGPIPE when piped to head breaks the
pipeline. A tool that emits ANSI color codes into a pipe pollutes downstream JSON parsing. A tool with inconsistent
subcommand naming forces the agent to memorize exceptions rather than apply patterns. Composability is what makes a CLI
tool a building block rather than a dead end.
Requirements
MUST:
-
A SIGPIPE fix is the first executable statement in
main(). Without it, piping output tohead,tail, or any tool that closes the pipe early causes a panic:unsafe { libc::signal(libc::SIGPIPE, libc::SIG_DFL); } -
TTY detection, plus support for
NO_COLORandTERM=dumb. When stdout or stderr is not a terminal, color codes are suppressed automatically. -
Shell completions available via a
completionssubcommand (clap_complete in Rust; equivalents elsewhere). This is a Tier 1 meta-command — it works without config, auth, or network. -
Network CLIs ship a
--timeoutflag with a sensible default (30 seconds). Agents operating under their own time budgets need to fail fast rather than block on a slow upstream. -
If the CLI uses a pager (
less,more,$PAGER), it supports--no-pageror respectsPAGER="". Pagers block headless execution indefinitely. -
When the CLI uses subcommands, agentic flags (
--output,--quiet,--no-interactive,--timeout) areglobal = trueso they propagate to every subcommand automatically.
SHOULD:
- Commands that accept input read from stdin when no file argument is provided. Pipeline composition depends on it.
- Subcommand naming follows a consistent
noun verborverb nounconvention throughout the tool. Mixing patterns (e.g.,list-usersalongsideuser show) forces agents to learn exceptions. - A three-tier dependency gating pattern: Tier 1 (meta-commands like
completions,version) needs nothing; Tier 2 (local commands) needs config; Tier 3 (network commands) needs config + auth.completionsandversionalways work, even in broken environments. - Operations are modeled as subcommands, not flags.
tool search "query"is correct;tool --search "query"is wrong. Flags modify behavior (--quiet,--output json); subcommands select operations.
MAY:
- A
--color auto|always|neverflag for explicit color control beyond TTY auto-detection.
Evidence
libc::signal(libc::SIGPIPE, libc::SIG_DFL)(or the equivalent in the target language) as the first statement ofmain().IsTerminaltrait usage (std::io::IsTerminalor theis-terminalcrate).NO_COLORandTERM=dumbchecks.clap_completeinCargo.toml.- A
completionssubcommand in the CLI enum. - Tiered match arms in
main()separating meta-commands from config-dependent commands.
Anti-Patterns
- Missing SIGPIPE handler —
cargo run -- list | headpanics with "broken pipe". - Hard-coded ANSI escape codes without TTY detection.
- Color output in JSON mode — ANSI codes inside JSON string values break downstream parsing.
- A
completionscommand that requires auth or config to run. - No stdin support on commands where piped input is a natural use case.
Measured by check IDs p6-sigpipe, p6-no-color, p6-completions, p6-timeout, p6-agents-md. Run
agentnative check --principle 6 . against your CLI to see each.