I’ve built two CLIs in the last few months and neither of them is for me. One wraps our internal admin API, the other ships codemods and docs for our design system. In both cases the primary user is an AI agent, and I only found out how different that is by watching agents fail at tools I thought were good.
Here’s the thing: everything we consider “good CLI design” — pretty tables, colors, interactive prompts, spinners, helpful confirmation dialogs — was designed for human eyes and human patience. An agent has neither. It has a context window it pays for by the token, a tendency to hallucinate flag names, and no ability to answer “Are you sure? (y/N)”.
So I’ve been collecting rules. Some I stole (Justin Poehnelt’s agent-first CLI post is great), some I learned by watching Claude mangle my output. Here’s what actually matters.
JSON in, JSON out
Every command returns JSON to stdout. Every error returns JSON to stderr. No exceptions, no --format json flag that half the commands forgot to implement.
The failure mode without this is subtle: the agent regexes your pretty table, gets it right in testing, then your column widths shift and it silently extracts the wrong account ID. Structured output isn’t a nice-to-have — it’s the difference between a tool and a trap.
Same on the way in. For mutations, take the raw API payload:
admin accounts update 123 --json '{"name":"New Name"}'
Not a flag per field. Agents are shockingly good at generating JSON that matches a schema and shockingly bad at guessing whether you called it --name, --account-name, or --set-name. Every bespoke flag is a chance to hallucinate.
Stable exit codes
Give errors a taxonomy and document it:
| code | meaning |
|---|---|
| 0 | success |
| 2 | validation error |
| 3 | auth error |
| 4 | authorization error |
| 5 | API error |
This turns error handling from “parse the message and guess” into a branch. Exit 3? Tell the human to re-auth. Exit 2? Fix the input and retry. Exit 5? Probably don’t retry the same request five times (they will anyway, but you tried).
The CLI is the docs
This one changed how I think about documentation entirely.
Docs written by hand go stale. Docs generated from the tool can’t. So the CLI should be able to describe itself, at runtime, from the source of truth:
admin schema accounts.update # what fields does this take?
admin schema # what endpoints exist?
And give agents one command that teaches them everything:
admin prime
prime dumps LLM-ready instructions — the full command surface, the exit codes, the conventions, the examples. Now your CLAUDE.md is one line (“run admin prime first”) instead of three paragraphs that were accurate in March. (Trevin Chow calls a similar pattern agent-context — same idea, whatever you name it.)
If your README says one thing and your binary does another, an agent will find that gap faster than any human user ever did. Generate the docs from the tool.
Respect the context window
Agents pay per token. Your 400-line JSON response with every field populated is real money and — worse — real noise pushing important stuff out of context.
Give them projection tools:
admin accounts list --fields "id,name,status"
admin accounts list --jq '.accounts[] | select(.status == "active")'
And for pagination, emit NDJSON with --page-all instead of making the agent loop. One line per record streams nicely and never builds a giant array in memory or in context.
The agent is not a trusted operator
My favorite rule, cuz it inverts how we usually think about users. A human who types accounts delete 123 probably means it. An agent that types it might be confidently wrong about which ID it’s holding.
So: --dry-run on every mutation. It validates everything and prints the exact request it would send — without sending it. The agent previews, the human (or the agent, on a second look) confirms, then it runs for real.
Validate aggressively too. IDs must be numeric. Paths can’t contain ... Payloads get checked for control characters. None of this is about malice — it’s about a model that’s 98% right being wrong 2% of the time at scale.
A variant I keep coming back to: have the dry-run hand back a confirmation token, and make a separate confirm command that takes it. Past me, pitching this to a teammate:
you could make it so there is another tool called confirm where [you] send that confirm token and then users could put the main execute command as always allowed and trust that the confirm one is the one that needs approval
Now the read/preview path can be on the agent’s allowlist and the only thing gated behind human approval is the confirm. One permission prompt, exactly where it matters.
Structured means testable
The sleeper benefit of JSON-in, JSON-out: you can eval it. When every mutation is a payload, you can assert the agent generated the right body and params — no screen-scraping, no “seems like it worked.” Past me, right when this clicked:
and this will be great for evals — we can verify it is sending the right body/params
There’s a subtler win too. Less to generate means less to get wrong. Here’s how I explained it in a DM:
with the cli the agent can use less words to do the same thing so it is more likely to get it right! but…we need to make sure that is still doing the right things because sometimes AI get confused so we do a slow rollout
That last part is the rule: don’t just ship the agent-facing interface, measure that agents actually do better on it. Structured output makes that measurable. Roll it out slow and check.
Skip the MCP server
I’ve been on this one for a while. When the “MCP is dead, long live the CLI” posts started making the rounds this year, a teammate asked if I’d secretly written one. I’ve held this take since MCP first came on the scene, tbqh.
Every agent has bash. Not every agent has your MCP server configured — and configuring it means tokens, config file paths, per-client setup, a running process. Our design system MCP server needs a GitHub token and an .npmrc path in every client on every machine. The CLI version of the same thing is pnpm exec pluma-cli component button and it works instantly for anyone whose repo already installs the package.
And there’s a quieter cost that compounds: every MCP tool you register stuffs more schema into the context window before the agent has done anything at all. Past me, pushing back on an MCP-first strategy doc:
The industry is now seeing some shift to CLIs more since it works with all the wonderful things bash has to offer, doesn’t fill up context as quickly as mcp, and is more discoverable to the LLMs. You can look at the context stuffing we had to do in the tools to get them to be called correctly. It only gets worse on each tool addition.
MCP still earns its keep for clients that can’t shell out. But build the CLI first and make MCP a thin wrapper over it, not the other way around.
Don’t do the model’s job
Last one. When you start building for agents it’s tempting to add commands that “help the AI” — scaffold generators, layout DSLs, commands that turn natural language into code. Skip them. The model is better at that than your CLI will ever be.
Your job is the stuff the model can’t do: real data, real mutations, real schemas, real validation. Be a good tool, not a bad model.
That’s the list so far. If you’re building a CLI right now, the cheapest place to start is --json everywhere and documented exit codes — you can retrofit those in an afternoon and every agent interaction gets better immediately.
I’ll leave you with past me, three minutes into using our admin CLI for the first time:
ok 3 minutes into using the cli and I’m convinced even more this is the right path for llms at the moment
Three minutes. That’s the bar. And if you’ve found rules I’m missing, I wanna hear them.