How to document a CLI tool developers actually use
CLI docs fail when they copy the API-reference playbook. Here's the five-section structure, auto-generated reference, and real terminal examples that work.
Run --help on most CLI tools and you get a wall of flags with no context, no examples, and no explanation of which three commands actually matter. Then you check the docs site and find an API reference with request/response schemas for a tool nobody calls over HTTP. CLI users don’t want a REST reference. They want to know what to type, in order, to get from a fresh install to a working result — and they want it in the same terminal window they’re already staring at.
Most teams write CLI docs like they write API docs: a reference page per command, generated once, rarely touched. That approach fails the exact person the CLI is for — someone who is currently in a terminal, mid-task, and will not tab away to read prose if they can avoid it.
Why CLI docs are different from API docs
API docs describe a contract between two programs. CLI docs describe a contract between a program and a human sitting at a keyboard, and that human is impatient in a specific way: they’re already running commands, they just typed something that didn’t work, and they want the fix in the next ten seconds.
A few consequences follow from that:
- There’s no browser tab open by default. An API consumer is already reading your reference in a browser while writing code in an editor. A CLI user is in a terminal, and pulling up docs means switching context entirely — so the tool itself has to carry more of the explanation (
--help,-h, error messages,<tool> man). - Output is the UI. API docs show you a request and a response body. CLI docs need to show you the actual terminal output — prompts, progress bars, colored diagnostics, exit codes — because that output is what the user sees and has to interpret.
- Copy-paste is the primary interaction. Nobody hand-types a
curlcommand from an API doc either, but with a CLI the copy-paste surface is almost the entire document: every example needs to be a command someone can paste into their shell and run unmodified. - State lives on disk, not in a request body. A CLI often reads config files, environment variables, and previous command state (
~/.tool/config.yml,TOOL_TOKEN, a lockfile). Docs have to cover where that state lives and how to reset it, which has no real API-doc equivalent. - Discoverability depends on the tool, not the docs site. Most users will discover subcommands through
<tool> --helpbefore they discover your website. If the docs and the--helpoutput disagree, the--helpoutput wins, and now your docs are the thing that’s wrong.
None of this means CLI docs need less structure than API docs — they need a different structure, one built around a terminal session instead of a request/response pair.
The five things every CLI doc needs
Strip away everything optional and a CLI reference needs exactly five sections. Skip any one of them and support tickets go up.
- Install — every supported method (
brew install,npm install -g, a curl-to-shell script, a downloadable binary), plus how to verify the install worked. “Runtool --versionand confirm you see2.4.1or later” saves more tickets than any amount of prose. - Quickstart — the shortest path from “just installed” to “saw something work.” Five commands, max. Not a tour of every feature — one real task, start to finish.
- Command reference — every subcommand, one entry each, with its own description, arguments, and a runnable example. This is the page people land on from a search engine six months after the quickstart.
- Flags — global flags (
--verbose,--json,--config) documented once in a shared table, plus per-command flags documented next to the command they modify. Don’t make someone guess whether--dry-runis global or scoped to one subcommand. - Exit codes — the one section almost every CLI doc skips, and the one that matters most the moment someone is scripting against your tool in CI. If
deploycan exit0,1,2, and78, write down what each one means. A pipeline author will grep your docs for this before they’ll read anything else.
Everything past these five — advanced config, plugin systems, shell completion — is real and worth documenting, but it’s secondary. If a new user can’t install, run a quickstart, look up a command, check a flag, and interpret an exit code, nothing else in the docs matters yet.
Auto-generating a command reference from —help output
The command reference is the one section you should almost never write by hand, because it’s the one section that’s guaranteed to drift. Every time someone adds a flag or renames a subcommand, the reference page goes stale unless something regenerates it.
Most CLI frameworks already expose everything you need through --help, so the reference can be built as a build step instead of a writing task:
$ mytool deploy --help
Usage: mytool deploy [flags]
Deploy the current project to the configured environment.
Flags:
-e, --env string Target environment (default "staging")
-f, --force Skip confirmation prompt
--dry-run Print the deploy plan without executing it
-o, --output string Output format: text, json (default "text")
-h, --help Show help for deploy
Examples:
mytool deploy --env production
mytool deploy --dry-run --output json
That output is structured enough to parse: usage line, description, a flags table, an examples block. A small script can walk every subcommand, capture --help for each, and emit Markdown:
#!/usr/bin/env bash
# gen-cli-reference.sh — regenerate docs/reference from --help output
set -euo pipefail
OUT="docs/reference"
mkdir -p "$OUT"
for cmd in $(mytool commands --list); do
{
echo "## $cmd"
echo
echo '```'
mytool "$cmd" --help
echo '```'
} > "$OUT/$cmd.md"
done
echo "Regenerated $(ls "$OUT" | wc -l) command reference pages"
Run that in CI on every release and the reference page can never fall out of sync with the actual binary — because it’s generated from the actual binary, not from someone’s memory of what the flags used to be.
❌ Hand-maintaining a flags table in a wiki page, updated “whenever someone remembers”:
## deploy
| Flag | Description |
|------|-------------|
| --env | the environment |
| --force | skips things |
That table is already wrong the moment --dry-run ships and nobody edits the wiki. It’s also thin — “skips things” tells a scripting user nothing about what gets skipped or why they’d want that.
✅ Generated from --help, checked into docs, regenerated on every release:
## deploy
Deploy the current project to the configured environment.
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `-e, --env` | string | `staging` | Target environment |
| `-f, --force` | bool | `false` | Skip confirmation prompt |
| `--dry-run` | bool | `false` | Print the deploy plan without executing it |
| `-o, --output` | string | `text` | Output format: `text`, `json` |
**Examples**
mytool deploy --env production
mytool deploy --dry-run --output json
This is also where GitDoc fits into a CLI’s docs, specifically: instead of a cron job that regenerates a reference page nobody reviews, GitDoc watches the repo where your CLI’s flag definitions live, and when a PR changes them, it opens a pending doc update against the reference page — so a human still reviews the diff, but nobody has to remember to run the script.
Writing examples that match real terminal sessions
The fastest way to lose a CLI user’s trust is to show them a sanitized command that doesn’t match what they’ll actually see when they run it. A doc that shows mytool deploy with no output, when the real command prints a progress bar, three warnings, and a confirmation prompt, teaches the reader that your examples aren’t reliable — so they stop trusting the next one too.
Capture the real session instead of typing an idealized one:
$ mytool deploy --env production
? This will deploy to production. Continue? (y/N) y
Deploying to production...
✓ Built assets (2.3s)
✓ Uploaded bundle (14.1s)
✓ Ran migrations (0.8s)
✓ Health check passed
Deployed v2.4.1 to production in 18.4s
View: https://app.example.com/deploys/8f21a0c
That block does four things a sanitized snippet doesn’t: it shows the confirmation prompt so users aren’t surprised by it, it shows realistic timing so users know a hang at 14s is normal, it shows the exact success message so users can grep their CI logs for it, and it shows a real exit artifact (the deploy URL) so users know what “done” looks like.
A few rules keep example output honest:
- Run the command, don’t write the output from memory. Memory drifts toward what the tool used to print, not what it prints now.
- Include the prompt character and the input.
$ mytool deployalone hides that this is interactive; showing? Continue? (y/N) ytells the reader what to expect and how to script around it (--force,-y, pipingyes). - Show at least one error case. A doc with only happy-path examples leaves users guessing the moment something fails. Include the actual error text so it’s greppable.
- Keep real IDs and timestamps out. Redact tokens, account IDs, and internal URLs, but keep the shape of the output — formatting, ordering, whether it’s colorized — intact.
- Refresh examples on major version bumps. If output formatting changes (a new progress indicator, a restructured JSON output), the old example is now actively misleading, not just outdated.
❌ A cleaned-up example that hides the interactive parts:
$ mytool deploy
Deployed successfully.
✅ The actual session, prompt and all:
$ mytool deploy
? This will deploy to production. Continue? (y/N) y
Deploying... done in 18.4s
The second version is barely longer and it’s the difference between a user who knows a confirmation prompt is coming and one who thinks the command hung.
Versioning CLI docs alongside CLI releases
A CLI’s docs are only correct for one version of the binary, and unlike a web API, users don’t all run the latest version. Someone on mytool@1.8 pinned in a Docker image for the last eight months is reading the same docs site as someone who ran brew upgrade this morning — and if the docs only describe 2.0, the 1.8 user is reading documentation for flags that don’t exist yet on their machine.
Three practices keep versioned CLI docs usable:
- Tag docs to release, not to date. Cut a docs snapshot at the same tag as the binary release (
v2.0.0), not on a separate quarterly schedule. If your release pipeline already tags a git ref, the docs snapshot should be generated from that same ref. - Show a version switcher, not just a “latest” page. A dropdown for
2.x,1.x,0.xcosts little to build and saves the exact support ticket that starts with “the docs say--configtakes a path but mine says unknown flag.” - Changelog every flag and command change, not just features. “Renamed
--outto--output(--outstill works but is deprecated)” is a one-line changelog entry that prevents a dozen “why doesn’t this work anymore” issues.
The install and quickstart pages deserve extra care here: they’re the pages new users hit regardless of which version they end up running, so they should default to “latest stable” and link out to older versions rather than silently describing whatever shipped last.
Versioning is also where auto-generated references pay off twice: if the command reference is generated from --help output as part of the release build, versioning it is just a matter of snapshotting that generated output per tag instead of hand-tracking which prose paragraph applies to which release. GitDoc does this by tying doc updates to the same commits and tags that produce your releases, so the reference a 1.8 user sees actually reflects the 1.8 binary, not whatever the flags looked like the last time someone manually edited the page.
CLI docs live or die on whether they match what’s actually in the terminal — the install command actually installs, the flag actually exists, the exit code actually means what the table says. That’s a harder bar to hit by hand than it looks, and it’s exactly the kind of drift GitDoc is built to catch. GitDoc keeps your docs in sync with your codebase on every push. Start free →