Documenting CLIs

This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page was written against the documentation of picocli, clap_mangen, Cobra and the Asciidoctor manpage backend as checked on 2026-09-30, and against the docsctl running example of this sub-section (fictional, illustrative). The generator script is a fragment that was not run, because docsctl does not exist; the tools it relies on are named as their documentation names them.

A CLI has several audiences and, usually, several copies of the same facts: the --help text, the man page, the reference page on the documentation site, the shell completion, the agent skill, and (when the tool is wrapped) the MCP tool descriptions and schemas. Every copy that is written by hand drifts, and drift is worse for an agent than for a person, because an agent trusts the text it reads. The rule of this page is the same as the one on Self-Describing Help: define the interface once, in the command tree, and generate everything else from it.

One source, many outputs

The command tree (commands, arguments, flags, defaults, descriptions, examples, exit codes, environment variables) lives in code, in the annotations or builders of the framework. The framework already renders it as --help; the rest is a question of which generator reads the tree next. docsctl schema --json, whose shape is defined in the sub-section’s contract, is the framework-independent form of the tree, and it is what the wrappers and the documentation build consume.

flowchart TD T["Command tree in code: commands, flags, examples, exit codes"] --> H["--help"] T --> C["Shell completion"] T --> M["Man pages"] T --> S["docsctl schema --json"] S --> R["Reference pages on the docs site"] S --> P["MCP tool definitions: inputSchema, annotations"] S --> K["Agent skill: command list and examples"] M --> D["Package: man and completion files"]
Figure 1. One command tree, several generated outputs

Generated files are either committed and checked in CI, or built on every release and never committed. Committing them makes reviews show interface changes; the price is a CI step that regenerates them and fails on a difference (git diff --exit-code docs/cli), so a stale file cannot merge.

Man pages and reference docs from the command tree

Each of the mainstream frameworks can walk its command tree and write documentation. The framework pages show the code in context; the summary here is what each generator produces and where its output goes.

Table 1. Documentation generators by framework
Framework Generator Output

picocli

picocli.codegen.docgen.manpage.ManPageGenerator from picocli-codegen

AsciiDoc man-page sources, one per command, to be converted with Asciidoctor’s manpage backend (see the Java page)

clap

clap_mangen (Man::new(command) then render), and generate_to for a command and its subcommands

ROFF man pages; the crate recommends running it from build.rs or a cargo xtask, not from the shipped program (see the Go & Rust page)

Cobra

The doc package: GenMarkdownTree, GenManTree (with a GenManHeader) and GenReSTTree

One Markdown, man or ReST file per command path (docsctl.md, docsctl_publish.md)

Cobra’s own guide on documenting CLIs for LLMs makes the useful points that Markdown, one file per command, is a good default because it chunks well, that DisableAutoGenTag should be set so that regeneration produces identical files with no timestamp footer, and that the quality of the output depends on the Short, Long and Example fields you fill in. The same is true of every framework: a generator publishes what you wrote in the descriptions and examples of the tree, so the effort goes there.

Generating Cobra documentation reproducibly (not compiled)
root := newRoot()
root.DisableAutoGenTag = true // no timestamp footer, so diffs stay empty
if err := doc.GenMarkdownTree(root, "./docs/cli"); err != nil {
	log.Fatal(err)
}
header := &doc.GenManHeader{Title: "DOCSCTL", Section: "1"}
if err := doc.GenManTree(root, header, "./docs/man"); err != nil {
	log.Fatal(err)
}

AsciiDoc man pages with the Asciidoctor manpage backend

When the framework does not generate man pages, or when you want the reference and the man page from AsciiDoc, use Asciidoctor’s manpage backend. The rules are few: the document title is the program name and section as progname(volume) with no spaces, a NAME section holds a one-line description in the form progname - purpose, a SYNOPSIS section is recommended, and the attributes manmanual, mansource and manversion fill in the header and footer of the rendered page. The output file name comes from the title, so docsctl(1) becomes docsctl.1.

docsctl.1.adoc (abridged)
= docsctl(1)
:manmanual: Docsctl Manual
:mansource: docsctl 0.1.0

== Name

docsctl - search and publish this site's documentation

== Synopsis

*docsctl* [_GLOBAL OPTIONS_] _COMMAND_ [_ARGS_]

== Exit status

*0*:: Success, including an idempotent no-op and a dry-run plan.
*2*:: Usage error.
*3*:: Validation problems found.
*5*:: Confirmation required; the error names the exact command that confirms.
*7*:: Authentication required.

== Environment

*DOCSCTL_TOKEN*:: Access token (never printed).
*DOCSCTL_NO_INPUT*:: When set to `1`, never prompt.
Converting and reading it
asciidoctor -b manpage docsctl.1.adoc     # writes docsctl.1
man ./docsctl.1

This site is built with Antora, which renders AsciiDoc pages to HTML; the man-page conversion is a separate step that you run with the standalone asciidoctor command in the tool’s own repository. The reference page for the documentation site and the man page therefore have different build paths. Avoid maintaining both by hand: generate both from the same source (the next section), so that the exit-status table on the man page is the exit-status table on the site. Antora resolves include targets by resource ID (such as partial$), and standalone Asciidoctor by file path, so a fragment shared between the two needs care and a check of both builds; generation from the tree avoids the question.

Man pages are worth the trouble because man docsctl works offline and is what packagers install (see Packaging & Distribution), and an agent that has a shell can read one with man docsctl | col -b. Windows has no man, so the equivalents there are --help and the published site.

Keeping help, docs and MCP schemas in sync

The synchronisation mechanism is a small program that reads docsctl schema --json and writes whatever is needed. Because the schema already carries the description, the arguments and flags, the exit codes and the safety metadata of every command, the same input feeds three outputs.

Reference page from the command tree (tools/gen_reference.py, abridged)
import json
import subprocess
from pathlib import Path

schema = json.loads(subprocess.run(
    ["docsctl", "schema", "--json"], capture_output=True, text=True, check=True, timeout=30,
).stdout)

def esc(text: str) -> str:
    return text.replace("|", "\\|")

out = [f"= {schema['name']} {schema['version']}", ""]
for cmd in schema["commands"]:
    out += [f"== {cmd['name']}", "", esc(cmd["summary"]), "",
            "[cols=\"2,1,4\",options=\"header\"]", "|===", "| Flag | Type | Description"]
    for flag in cmd["flags"]:
        out.append(f"| `{flag['name']}` | {flag['type']} | {esc(flag['description'])}")
    out += ["|===", "",
            "Exit codes: " + ", ".join(str(c) for c in cmd["exitCodes"]), ""]
Path("modules/ROOT/pages/reference.adoc").write_text("\n".join(out) + "\n")

The other two outputs read the same file. The MCP wrapper maps each command to a tool: the arguments and flags become the inputSchema, the output.schema becomes the outputSchema, the command summary becomes the description, and safety becomes the tool annotations. The mapping is specified in Wrapping a CLI as an MCP Server, and the annotation names are those of the MCP primitives page and the MCP tools specification. The agent skill (see Self-Describing Help) takes its command list and examples from the same file, so it cannot advertise a flag that no longer exists.

Four checks keep the pipeline honest.

  • Regenerate and diff in CI. The build fails when generated files differ from those committed, which is the signal to run the generator.

  • Parity test for the wrapper. A test compares the tools an MCP wrapper exposes with the commands in schema --json, minus the ones it deliberately withholds (login and logout, as Wrapping a CLI as an MCP Server advises), so a new command is either exposed or consciously excluded.

  • Examples run. Every example in --help and in the reference is executed in the black-box suite of Testing CLIs against a fixture, so an example that stopped working fails a test rather than misleading a reader.

  • One version number. The version in the man page, the site and the schema comes from the tool’s own --version.

Writing what the generators cannot

A generator cannot supply intent. Three pieces of prose are written by hand, once, next to the code that they describe.

  • Descriptions that say when to use the command, not only what it is called ("Search the published documentation; use show to read a result"), because these become the MCP tool descriptions and what an agent chooses on.

  • Examples with real inputs and their output shape, one or two per command.

  • Exit-code and error-code tables, kept as data in the code so that the man page, the reference and the --help footer print the same table.

Longer material (tutorials, the agent-facing guide, the reasoning behind design choices) belongs on a documentation site, linked from --help and from the man page’s SEE ALSO section. Keep it short and let the generated reference carry the details, in the manner of the token budget rules: what an agent reads should be the least that lets it act.

Checklist

  • --help, completion, man pages, the reference and the MCP definitions are all generated from the command tree.

  • Generation is reproducible (no timestamps) and CI fails when generated files are stale.

  • Every command has a description that says when to use it and at least one runnable example.

  • The exit-code, error-code and environment-variable tables are data, printed identically everywhere.

  • Man pages and completions ship in the package.

  • The list is condensed on the Agent-Readiness Checklist.