# How to write CLI help that people and agents can both use

> Describe the command surface once, render it as text and as data, and lint the result so a command cannot ship with an example that does not run.

Source: https://voxgig.com/howto/help-text-people-and-agents-can-use

- Audience: api-producer
- Level: beginner
- Languages: typescript, javascript
- Verified: 2026-09-14
- Published: 2026-09-14
- Updated: 2026-09-24

## Short answer

Keep one description of the commands, their arguments, their flags and their examples, and render the human help and the machine help from it. Then lint that description. Every command needs a short summary, every argument and flag needs a line, and every example must match the command it claims to demonstrate. A destructive command must document its confirmation flag.

---
## You will need

Node 22 or later, and a CLI with more than one command. The conventions for what belongs in help
come from [the command line interface guidelines](https://clig.dev/#help), and the shape of the data
rendering is whatever your callers parse, which for agents is usually JSON.[^1] The [GNU
standards](https://www.gnu.org/prep/standards/html_node/Command_002dLine-Interfaces.html) cover the
conventions a shell user expects, and the two sets of advice mostly agree.

## Approaches compared

| Approach | When it fits | What it costs you | When to pick something else |
| --- | --- | --- | --- |
| [A separate agent instructions file](https://agents.md/) | The CLI is stable and you want conventions and context beyond usage | A second document that drifts from the tool with nothing checking it | The usage itself is what agents keep getting wrong |
| [Generated from the parser](https://nodejs.org/api/util.html#utilparseargsconfig) | You already declare options in code and want no second source | Examples and summaries still live somewhere, usually in comments nobody lints | You want examples checked against the commands |
| [Help strings written by hand](https://www.gnu.org/prep/standards/html_node/Command_002dLine-Interfaces.html) | A single-command tool whose usage fits in four lines | Help that drifts the first time a flag is added in a hurry | The tool has several commands, or more than one maintainer |
| [One spec rendered two ways](https://clig.dev/#help) | Several commands, and readers who are both people and machines | A description to keep current, which is the point rather than the cost | The tool has one command and two flags |

The argument for a single description is not that writing help twice is hard. It is that two copies
disagree, and the copy an agent reads is the one nobody proofreads.[^2] When a flag is added, a spec
makes the omission a lint failure rather than a discovery.

Examples are the part worth the most and the part that rots fastest. An example is the shortest path
from reading to doing, whether the reader is a person or an agent. It is also the only piece of help
that can be checked mechanically against the commands it describes.

## Describe the surface once

Everything both renderings need, in one place.

```js title="spec.mjs"
{
  name: 'retire',
  summary: 'Retire a meter, which cannot be undone',
  args: [{ name: 'id', summary: 'The meter id to retire' }],
  flags: [{ name: 'yes', type: 'boolean', summary: 'Confirm without a prompt' }],
  examples: ['meters retire mtr_8f2 --yes'],
  destructive: true,
},
```

The `destructive` flag is metadata rather than prose. A person reads the summary and understands; an
agent reads the field and can apply a rule, such as asking its user before running anything marked
that way.

Keep summaries short enough to fit a listing. Sixty characters is roughly what a command list can
show without wrapping. A summary that needs more than that is usually two commands wearing one name,
and splitting them is the real fix.

## Lint the help like code

Six rules, each named after the confusion it prevents.

```js title="lint.mjs"
if (!c.examples?.length) say(c.name, 'no example, so a reader has to infer the argument order')
```

Check the examples against the spec rather than only checking that they exist. An example with the
wrong argument count or a flag the command does not accept is worse than none, because a reader
trusts it and an agent runs it.

Run the lint in the test suite. Help is part of the interface, and treating it as testable is what
stops it drifting between releases.

## Check it worked

Render both forms, then lint a good spec and a careless one.

```bash
node demo.mjs
```

```text output
meters - Read and retire electricity meters

Commands:
  list           List meters, newest first
  show <id>      Show one meter by id
  retire <id>    Retire a meter, which cannot be undone

Examples:
  meters list --limit 10
  meters show mtr_8f2
  meters retire mtr_8f2 --yes

the same surface as data
  commands list, show, retire
  destructive retire
  flags of retire --yes

lint of the help
  no findings

lint of help that was filled in rather than written
  export: summary does not start with a capital
  export: example "meters export --format csv" passes 0 arguments, the command takes 1
  export: argument path has no summary
  purge: summary is 74 characters, over 60
  purge: no example, so a reader has to infer the argument order
  purge: is destructive and documents no confirmation flag
```

The last block is what a first draft of help usually looks like. Every finding is specific: the
example is missing a required argument, the summary would wrap in a listing, and a command that
deletes things documents no way to confirm. None of those needs a human to spot.

The two renderings before it are the same surface. The text lists three commands with their usage;
the data names which command is destructive and which flags it takes. Neither can describe something
the other does not, because both come from the same object. That is the whole mechanism, and it is
about forty lines.

```bash
node --test lint.test.mjs
```

```text output
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 114.980464
```

## When it goes wrong

The spec and the parser disagree. Two declarations of the same flags. Build the parser from the
spec, or assert in a test that every parser option appears in the spec.[^3] The second is a few lines
and it catches the flag somebody added in a hurry.

Help is correct but goes unread. The listing is forty commands long. Group them, and let the
top-level help show the groups rather than every command. A second level of help is cheaper than a
listing people scroll past.

An agent uses a flag that does not exist. It generalized from another command in your tool.
Consistency is a help problem too, so name the same idea the same way everywhere. A flag called
`--json` on three commands and `--format json` on a fourth is a guess waiting to happen.

Examples use placeholder values. `<your-id-here>` cannot be run. Use a realistic value in the shape
your ids take, so the example works after one substitution. An example that runs unchanged against a
sandbox is better still.

## When not to do this

Do not build a spec for a tool with one command. A usage line and two flags do not drift, and the
machinery costs more than it saves.

Do not put paragraphs of explanation in command help. Help answers what the arguments are and what
one invocation looks like, and anything longer belongs in the documentation the help links to. A
help page nobody can read at a glance gets skipped, and then the example is all anyone sees.

Do not describe the surface twice to get machine-readable output. Two copies drift, and the one an
agent reads is the one nobody notices is wrong.

## Related how-tos

- [Design a CLI a coding agent can drive](/howto/design-a-cli-a-coding-agent-can-drive)

- [Test interactive prompts and terminal behavior in a CLI](/howto/test-interactive-prompts-and-tty-behaviour)

## Last verified

Verified 2026-09-14 against Node 22.22.2. Both output blocks are what the preceding command printed.

[^1]: The [guidelines](https://clig.dev/#help) want help behind both `-h` and `--help`, and a
concise version by default when a command that needs arguments is run without any. That concise text
has four parts. A description of what the program does, one or two example invocations, descriptions
of the flags unless there are lots of them, and an instruction to pass `--help` for more. They add
that adding `-h` to the end of anything should show help, whatever else is on the line, and that
`-h` is not to be overloaded. The example they hold up is `jq`, whose bare invocation prints an
introduction, one example, and the instruction.

[^2]: [AGENTS.md](https://agents.md/) describes itself as a README for agents, and says its authors
chose a name and format that could work for anyone rather than introducing another proprietary file.
It puts a count of adopting projects on its front page. Its sample file opens with three commands to
run, under a heading that says so. That is help text by another name, kept in a second file so that
the first can stay short for people.

[^3]: Node's own parser, `util.parseArgs`, arrived in 18.3.0 and 16.17.0 and stopped being
experimental in 20.0.0, according to [its history
table](https://nodejs.org/api/util.html#utilparseargsconfig). It takes an `options` object whose
keys are the long names of the flags. The values say the type, boolean or string, a short alias, a
default, and whether the flag may repeat. Nothing in that object is a summary, an example, or a
sentence for a person. Help generated from the parser therefore has to come from somewhere else.