Command-Line Tool
Command-line tools are where agents look best and behave worst. No UI to fuss over, so an agent will hand you something runnable in minutes. Then you pipe its output into another command and discover the "table" is decorated with box-drawing characters, errors print to stdout where they poison the pipe, and the exit code is 0 no matter what happened, so your script can't tell success from disaster.
The fix is knowing that a CLI has a contract, and the contract is older than you are. Arguments and flags follow conventions. --help exists and tells the truth. Exit code 0 means success and nonzero means failure, because that single integer is how every shell script, cron job, and CI pipeline ever written decides what to do next. Human-readable output goes to stdout, diagnostics go to stderr, and a --json flag gives programs a stable shape to parse. None of this is hard. All of it gets skipped when the spec doesn't demand it.
This template is the spec that demands it. Fill in what your tool actually does; the contract section rides along unchanged, and it is the part doing the work. A CLI that honors the contract composes with fifty years of existing tooling. One that doesn't is a program you can only operate by hand, which on a command line is a strange thing to have built.
Prerequisites
- A language runtime installed for whichever you pick below (Python 3.11+ or Node 20+ both work; the spec defaults to Python).
# Project: CLI tool, `[toolname]`
Build a command-line tool that [one sentence: what it does, e.g.,
"converts CSV exports from my bank into a tagged monthly spending
summary"].
## Language and packaging
- Python 3.11+ with `argparse` from the standard library. [Or: Node 20+
with `commander`.] No frameworks beyond that.
- Installable so `[toolname]` works as a command (`pipx install .` /
`npm link`), and also runnable directly from a fresh clone.
## Interface
```
[toolname] [command] [arguments] [flags]
```
Commands:
- `[command-one] <input>`: [what it does. e.g., `summarize
transactions.csv` prints a monthly summary]
- `[command-two] ...`: [second command, or delete this line; a
single-purpose tool with no subcommands is fine]
Flags:
- `--json`: machine-readable output (see Output contract).
- `--quiet`: suppress progress/diagnostic messages, results only.
- `--output <path>` / `-o`: write results to a file instead of stdout.
- `[--tool-specific-flag]`: [what it changes, and its default]
- `--help` / `--version`: standard behavior, on the tool and on every
subcommand.
## Behavior contract (non-negotiable)
- Exit codes: 0 success; 1 the operation failed (bad input data,
processing error); 2 the invocation was wrong (unknown flag, missing
argument). Never exit 0 when the work did not happen.
- stdout carries results. stderr carries everything else: progress,
warnings, errors. `[toolname] ... | next-command` must receive only
results.
- `--json` prints a single JSON object to stdout:
`{"ok": true|false, "data": {...}, "errors": [...]}` and nothing
else. No banner, no color codes, no progress text on stdout.
- Errors are one clear sentence to stderr: what failed, on which
input, and when possible what to do about it. Stack traces only
behind a `--debug` flag.
- Reading from stdin: if `<input>` is `-`, read stdin. Standard
convention; it makes the tool pipeable on the input side too.
- No interactive prompts unless a flag explicitly asks for them. The
tool must run unattended in a script or cron job.
- Same input, same output. If the tool touches anything nondeterministic
(time, randomness), a flag pins it (`--seed`, `--now`) so runs are
reproducible in tests.
## Core logic
[Describe the actual work in plain steps. Example for the CSV tool:
1. Parse the CSV; expect columns [date, description, amount].
2. Reject the file with exit 1 and a clear message if columns are
missing, and name the missing ones.
3. Group by month, tag rows by [rules], sum per tag.
4. Output: table to stdout, or the JSON shape above with --json.]
## Tests (write alongside, not after)
- One test per exit-code path: success is 0, bad data is 1, bad
invocation is 2.
- A test that `--json` output parses as JSON and matches the
documented shape.
- A test that stdout contains no diagnostics when piped (capture
stdout and stderr separately, assert on both).
- Golden-file test: a checked-in sample input and its expected
output, compared exactly.
## Done means
- Fresh clone, install, `[toolname] --help` works and is accurate.
- All tests pass; `echo $?` after a forced failure prints nonzero.
- `[toolname] [command] sample-input | cat` shows clean, parseable
results with zero decoration mixed in.Adaptation notes:
- Tool that changes things (deletes files, calls APIs, writes to a database): add a
--dry-runflag that prints what would happen and changes nothing, and make dry-run the behavior you test first. Destructive-by-default CLIs are how people learn about backups. - Long-running work: progress goes to stderr so it never contaminates piped output, and only when stderr is a terminal; detect that rather than always printing.
- Wrapping an existing API in a CLI: the exit-code and stdout/stderr contract is the whole value. The API errors are already good; your job is translating them to codes and clean messages.
- The mistake: treating
--helpas documentation that can drift. Help text describing flags that no longer exist is worse than no help text; the golden-file test should include the--helpoutput.