Author a CLI.md (AIP-29)
Use this skill when the user asks to wrap a third-party CLI so an agent can install, authenticate, sandbox, and invoke it safely. The skill produces a valid AIP-29 CLI.md bundle plus one TOOL.md per subcommand wrapped.
When to use
- "Wrap the GitHub CLI so the agent can list and merge PRs."
- "I want our agent to be able to call ffmpeg in a sandbox."
- "Add stripe CLI as an integration the billing agent can use."
When NOT to use
- The CLI exposes an HTTP API the agent could call directly → prefer the HTTP route. CLI.md is for binaries; HTTP integrations belong in a TOOL.md per endpoint.
- The user wants to wrap a single command with no auth/sandbox context → just author a TOOL.md (AIP-14) directly.
- The user wants to wrap an entire interactive shell session (REPL, TUI) → CLI.md is for one-shot invocations. Interactive sessions need a separate spec (under discussion).
Process
Follow these steps in order. Each step has a short justification — keep them in the file you produce so reviewers see why each field ended up the way it did.
1. Fix identity
id: the binary name, lowercase (gh,gcloud,kubectl,ffmpeg).bin: same asidfor binaries on$PATH. Vendored binaries use the workspace path (./bin/acme).name: human display name ("GitHub CLI", "FFmpeg").description: one paragraph for an LLM caller — what problems this CLI solves, what it doesn't.
2. Map install paths
Open the CLI's official install docs. Author 3+ install methods covering the major package managers and a fallback download URL:
install:
- { method: brew, package: gh }
- { method: apt, package: gh }
- { method: choco, package: gh }
- { method: download, url: "...", extract_bin: "...", verify_sha256: "..." }
For download and curl methods, always include verify_sha256
when production-ready. Without it, hosts will refuse to install in
secure environments. Find the SHA-256 in the upstream release page or
compute via shasum -a 256 <file>.
3. Author version detection
Run <bin> --version and capture the output:
$ gh --version
gh version 2.40.1 (2026-04-20)
…
Author the regex:
version_check:
cmd: "gh --version"
parse: 'gh version (\S+)'
range: ">=2.40 <3"
Pick the lowest version that supports the subcommands you intend to wrap. Pick the upper bound as the next major (open question — see AIP-29 § Open questions).
4. Map the auth surface
Read the CLI's auth docs. Identify:
- State location: which paths the CLI writes (
~/.config/gh,~/.aws/credentials). - Env vars: which env vars the CLI reads (
GH_TOKEN,STRIPE_API_KEY,KUBECONFIG). - Login flow: the exact command + interactivity needs.
- Refresh: cadence + command (often
<bin> auth refreshor similar). - Expiry signal: which exit code / error string indicates "auth expired".
Author the SECRETS.md inventory (AIP-19) listing all
env vars + their resolution sources (vault slugs, OAuth bindings).
Reference it from auth.ref:.
5. Author the sandbox profile
This is the most important step. Get it wrong and the bundle either breaks (missing permission) or escalates privilege (too permissive).
For each axis:
- Network egress: Run the CLI under network monitoring (
tcpdump,mitmproxy, or just observe). List every hostname it contacts. Avoid wildcards; be specific. - FS read/write: Read the CLI's source / docs / man page. List
config file paths, cache paths, and any working directories. Add
workspace paths (
./inventory/**) the user data lives in. - FS deny: Always
~/.ssh/**,/etc/**,~/.aws/**(unless this CLI is the AWS CLI). Belt-and-braces. - Exec: Default
false. Some CLIs spawn child processes —ghshells out togit,kubectlshells out to plugins. Allowlist the specific bin names. - Env: List vars the CLI requires; default-deny the rest.
- TTY: True only when login or some subcommand reads stdin interactively.
Test the sandbox: spin up a host, install the CLI, run the documented subcommands, and verify nothing fails permission-denied. If it does, add the missing permission — but be specific (a path, a hostname, not a wildcard).
6. Author output conventions
Run common subcommands. Note:
- Where success goes (stdout? stderr?).
- Where errors go.
- Whether
--json/--format=jsonworks. - Exit codes for success, error, auth-required.
Author output: accordingly. The standard exit codes (0=ok, 1=error,
2=usage, 4=auth_required, 124=timeout, 137=killed) MUST be respected;
add CLI-specific codes only when the CLI truly diverges.
7. Walk the subcommand tree
For each subcommand in scope (or, if empty, the documented root
subcommands):
- Run
<bin> <subcmd> --helpto capture flags + args. - Author a TOOL.md sibling at
./tools/<subcmd>/TOOL.mdusing the AIP-14 author-tool skill. - Set the tool's
runner.cli:ref back to the parent bundle. - Author the
runner.argv:template using${input.X}interpolations for input properties. - Add the tool to the bundle's
commands:tree.
Don't try to wrap every subcommand on day one. Start with the top 5–10 most-used subcommands and grow incrementally.
8. Author optional INTENTs
For common user-facing intents the bundle exposes (e.g. "open a PR with
a single click"), author INTENT.md entries under
./intents/<id>/. Each intent's implements: block points at one or
more subcommand tools inside the bundle. Reference them in the
bundle's intents: block.
This is optional — many bundles ship without pre-wired intents. Add them when the bundle is consumed by surfaces that need user-facing copy (chat catalog, menu).
9. Validate
Run the manifest through
./resources/aip-29/draft/CLI.schema.json:
ajv validate -s CLI.schema.json -d .cli/<id>/CLI.md \
--remove-additional fail \
--strict
Validate every per-subcommand TOOL.md against the AIP-14 schema. Reject the bundle if any sibling fails.
10. Wire to the host
import { loadCli, installCli, verifyCli, runCliTool } from "@agentproto/cli-runtime"
const bundle = await loadCli("./.cli/gh/CLI.md")
await installCli(bundle)
await verifyCli(bundle)
// First-call login (deferred until first tool invocation needs it)
const result = await runCliTool(bundle, "pr.list", {
input: { state: "open" },
context: { user: { id: "u_abc" }, surface: "chat" },
})
The bundle now installs on demand, version-checks, drives auth, and dispatches subcommands through the sandbox.
Output structure
The skill emits at minimum:
.cli/<id>/
CLI.md ← always
SECRETS.md ← always (AIP-19 inventory)
tools/
<subcmd-1>/TOOL.md
<subcmd-2>/TOOL.md
…
cli.ts ← only when custom login/parser needed
intents/ ← only when pre-wired INTENTs included
<intent-1>/INTENT.md
cli.ts is needed when:
- The CLI's output format isn't text/JSON/YAML (custom delimiter, pseudo- CSV, etc.).
- The login flow needs callback-URL allocation or token refresh logic beyond simple env vars.
- Exit-code → semantic mapping needs context-sensitive rules.
Otherwise the manifest stands alone.
Common mistakes
- Wildcarding
network.egress.["*"]is a bug. Be specific. If the CLI legitimately needs the open internet (yt-dlp,curlwrappers), document that in the body and set bundle-wide tools torisk_level: 2+. - Forgetting
state.pathsinauth. The CLI's stateful auth files MUST appear in BOTHauth.state.pathsANDsandbox.fs.read/write. Otherwise login appears to succeed but state never persists. - Shell metacharacters in
runner.argv. Tools using&&,|,;,>rely on shell features the runner doesn't provide. Compose multi-step invocations via WORKFLOW.md (AIP-15). - Missing
verify_sha256fordownload/curl. Production hosts refuse without it. Always supply. - Mutable
id. Renaming = breaking change = major bump + alias for the legacy id. - Wrapping every subcommand on day one. A 50-tool bundle is a 50-tool review. Start with the top 5–10; grow as needed.