Skip to content
Back to skills

Build Cli

ASecurity

Use when creating or extending a command-line tool in TypeScript, Go, Python or Rust — subcommands, typed args, config precedence, stdout/stderr split, --json, exit codes, NO_COLOR/TTY handling, --dry-run, and spawn-the-binary tests.

  • 5 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 10, 2026
developmenttypescriptpythonrustgobashnodegitsecurity

Works with

  • terminal
  • cli

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 4 files and shows the line behind each finding

Scanned October 10, 2026

npx -y skills add VelimirMueller/lab-claude-skills --skill build-cli --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Build Cli?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Build Cli
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/velimirmueller-build-cli/badge)](https://www.skillsdirectory.com/skills/velimirmueller-build-cli)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: build-cli
description: Use when creating or extending a command-line tool in TypeScript, Go, Python or Rust — subcommands, typed args, config precedence, stdout/stderr split, --json, exit codes, NO_COLOR/TTY handling, --dry-run, and spawn-the-binary tests.
---

# Build CLI

A CLI is called by people and by scripts. This skill makes the same tool work for both: one entry, subcommands, typed arguments, a fixed config order, data on stdout, diagnostics on stderr, `--json` with a stable schema, documented exit codes.

Rules with reasons: [cli-ux.md](cli-ux.md). Library choice and structure: [cli-patterns.md](cli-patterns.md). Tested code per language: [cli-examples.md](cli-examples.md). Versions: [stack-versions.md](../_shared/stack-versions.md) (verify live first, per [version-protocol](../../core/_shared/version-protocol.md)).

## 1. Audit current state

Change nothing yet. Read `.claude/stack-profile.md` (then `~/.claude/stack-profile.md`); if absent, detect.

```bash
cat .claude/stack-profile.md 2>/dev/null | sed -n '1,30p'
ls package.json go.mod pyproject.toml Cargo.toml 2>/dev/null          # language
grep -nE '"(commander|citty|yargs|cac|clipanion)"' package.json 2>/dev/null
grep -rnE 'spf13/cobra|urfave/cli|alecthomas/kong' go.mod 2>/dev/null
grep -nE '(typer|click|cyclopts|argparse)' pyproject.toml 2>/dev/null
grep -nE '^(clap|argh|lexopt)' Cargo.toml 2>/dev/null
ls src/cli* src/main.* cmd/*/main.go src/*/cli.py 2>/dev/null          # existing entry
grep -rnE 'NO_COLOR|isatty|IsTerminal|isTTY|--json' src cmd internal 2>/dev/null | head
```

Record: language, existing parser (keep it, see step 3), whether an entry file exists, whether `--json`, exit codes, config loading, tests-by-spawning are present. A monorepo with several languages: build the CLI in the language of the package that owns it; one question only if two candidates remain.

## 2. Decide what to do

- No CLI yet → full scaffold (steps 3-7).
- CLI exists, contract gaps → apply only the missing seams (step 5). Name each gap you will close.
- Contract met (stdout/stderr split, exit codes documented, `--json` schema, spawn tests) → say "already in place" and stop.
- Existing parser differs from the default (Click, yargs, urfave v2, clap 3) → keep it. Do not migrate inside this task.

## 3. Detect the track

Pick from `languages` in the profile or from step 1:

| Language | Parser | Config | Tests |
|---|---|---|---|
| TypeScript | `commander` 15, ESM, Node >= 22.18 | `smol-toml` + `env-paths` | Vitest spawning `dist/cli.mjs` |
| Go | `cobra`, entry `cmd/<name>/main.go`, `Run(args, out, err) int` seam | `go-toml/v2` | `go test` on `Run` + one built-binary test |
| Python | `typer`, `uv_build`, entry `<pkg>.cli:main` | `tomllib` + `platformdirs` | pytest + `subprocess` on `python -m <pkg>` |
| Rust | `clap` derive, `thiserror` | `toml` + `serde` + `dirs` | `assert_cmd` + `insta` help snapshot |

A profile with several languages gets one CLI per package, each following its own row. Lint and format come from the profile's `lint_format`.

## 4. Install only what is missing

Run the live check first (`npm view commander version`, `go list -m -versions github.com/spf13/cobra`, PyPI JSON, crates.io), then add dependencies with the package manager from the profile.

```bash
pnpm add commander env-paths smol-toml && pnpm add -D tsdown vitest   # TypeScript
go get github.com/spf13/cobra github.com/pelletier/go-toml/v2 golang.org/x/term   # Go
uv add typer platformdirs && uv add --dev pytest                       # Python
cargo add clap --features derive,env,wrap_help && cargo add serde --features derive && cargo add toml thiserror dirs   # Rust
```

TypeScript: runtime libraries go in `dependencies` (tsdown bundles `devDependencies`). Python: also set `requires = ["uv_build>=<current>,<next minor>"]`.

## 5. Generate the seams

Four seams per CLI, copied from [cli-examples.md](cli-examples.md) and renamed. Each owns one concern; nothing else touches it.

1. **`exit`** — an enum of codes (0 ok, 1 failure, 2 usage, domain codes from 3) and one typed error carrying a code and a hint.
2. **`output`** — the only writer to stdout/stderr. Decides colour (`NO_COLOR`, `TERM=dumb`, TTY per stream) and `interactive()` (stdin and stdout TTY, `CI` unset). Secrets are never passed to it.
3. **`config`** — builds a typed `Config` once: flags > env (`<NAME>_<KEY>`) > project `.<name>.toml` > user `$XDG_CONFIG_HOME/<name>/config.toml` > defaults. Unknown keys and bad values fail with exit 2.
4. **`cli` (entry)** — builds the parser, runs the command, maps `CliError` to its code, prints through `output`. The only place that exits.

Commands are thin: parse, call domain code, print through `output`. Then, for every command:

- Global flags `--json`, `--quiet`, `--verbose`, `--no-color` valid before and after the subcommand; `--version`; exit codes listed in the root `--help`.
- `--json` prints one document `{"schema": 1, ...}` on stdout, nothing else.
- Destructive commands: `--dry-run` (shared planning step, final write skipped) and `--yes`; prompt only when interactive, else fail with exit 2 naming the flag.
- Long options first; the same flag means the same thing everywhere.

## 6. Wire it

- Entry point: TypeScript `"bin": {"<name>": "./dist/cli.mjs"}` + `tsdown`; Python `[project.scripts]`; Go `cmd/<name>/main.go`; Rust `[[bin]]` (default).
- Task verbs (`dev`, `test`, `build`, `check`) go through the repo's runner: see [set-up-dev-toolchain](../set-up-dev-toolchain/SKILL.md). Release: [release-cli](../release-cli/SKILL.md).
- README: install, quick start, the config precedence table, the exit-code table, the JSON schema version. Link to `--help`, do not copy it.
- Completions: cobra and Typer have them built in, clap adds `clap_complete`; see `cli-patterns.md`.

## 7. Verify

Run the real binary, not the handler:

```bash
mycli --help; echo "exit=$?"                       # exit=0, lists exit codes
mycli list --json | jq -e '.schema == 1'            # stdout is pure JSON
mycli list --json >/dev/null 2>err.txt; test ! -s err.txt && echo clean   # nothing leaked to stderr
mycli list --bogus >/dev/null; echo "exit=$?"       # exit=2, stdout empty
NO_COLOR=1 mycli delete missing 2>&1 | grep -c $'\x1b'   # 0 escape sequences; the command exits 3
mycli delete x </dev/null; echo "exit=$?"           # not interactive: no hang
hyperfine --warmup 3 'mycli --version'              # under ~100 ms
```

Then the project check (`mise run check`, `pnpm test`, `go test ./...`, `uv run pytest`, `cargo test`) and the spawn tests from [cli-examples.md](cli-examples.md) pass. Expected: all exit codes as documented, `--help` snapshot committed.

## References

- [cli-ux.md](cli-ux.md): the rules and their reasons (streams, exit codes, JSON schema, config order, colour/TTY, dry-run, errors, help, speed, tests).
- [cli-patterns.md](cli-patterns.md): library choice per language, seams, completions, prompts.
- [cli-examples.md](cli-examples.md): the tested reference CLI in all four languages.
- [../_shared/stack-versions.md](../_shared/stack-versions.md): verified versions.
- [../../core/_shared/engineering-principles.md](../../core/_shared/engineering-principles.md), [../../core/_shared/security-baseline.md](../../core/_shared/security-baseline.md): seams, secrets.

Files in this skill

  • SKILL.md7.2 KB
  • cli-examples.md34.3 KB
  • cli-patterns.md6 KB
  • cli-ux.md11.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.