An agent reads your help, copies the example, and the command fails because the example is from two releases ago. A new engineer does the same thing an hour later. Nothing checked the example against the parser, because help is a string and strings do not get tested.
What you get
You will end up with one description of the command surface, two renderings of it, and a lint that fails when an example does not match the command it claims to show. This is for you if your CLI has grown past the point where one person remembers every flag.
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, and the shape of the data rendering is whatever your callers parse, which for agents is usually JSON.1 The GNU standards 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 | 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 | 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 | 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 | 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.
{
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.
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.
node demo.mjs
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.
node --test lint.test.mjs
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
Last verified
Verified 2026-09-14 against Node 22.22.2. Both output blocks are what the preceding command printed.
Footnotes
-
The guidelines want help behind both
-hand--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--helpfor more. They add that adding-hto the end of anything should show help, whatever else is on the line, and that-his not to be overloaded. The example they hold up isjq, whose bare invocation prints an introduction, one example, and the instruction. ↩︎ Back to text -
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. ↩︎ Back to text
-
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. It takes anoptionsobject 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. ↩︎ Back to text