Agents can't click your dashboard, but they can drive your CLI
Every internal tool now has a class of user that cannot see a screen. The CLI, long treated as the fallback interface, is the one they can operate, and most CLIs were never built to be operated.
An engineer wires an agent into a routine job: publish an updated API spec whenever a branch merges. The agent runs the publish command, reads the output, and reports back. The command prints a cheerful green “Published” and exits with status zero. The agent reports success. Everyone moves on.
What the command did not make obvious, because it was written for a person who would read the whole screen, is that the publish went through with a warning. The new version introduced a breaking change, and the tool surfaced that as a line of yellow text above the tick. A human would have seen the yellow. The agent saw exit code zero and the word “Published”, because those were the parts with a machine-readable meaning, and there was no machine-readable meaning attached to the warning at all. The breaking change shipped. The tool was not wrong, exactly. It answered a different question than the one the agent asked, in a language only a human eye could read.
This is the failure mode of automating against an interface designed for someone with eyes.
sequenceDiagram
participant A as Agent
participant CLI as Publish CLI
A->>CLI: publish the new spec
CLI-->>A: green "Published", a breaking-change warning in yellow, exit 0
Note over A: reads exit 0 and "Published" — the warning has no machine meaning
A->>A: reports success
Note over A: the breaking change ships, unnoticed
Human-first interfaces have a forgiving reader
The reason this persists is that CLIs were built for humans at a terminal, and humans are extraordinarily forgiving parsers.
A person reads around a spinner without thinking about it. They infer severity from colour, skim a table for the one number that matters, and register “succeeded with a warning” at a glance even when the exit code says nothing about it. Because the human reader is so capable, the interface never had to make its meaning explicit. Meaning lived in layout, colour and prose, all of which are free for a person and almost useless to a machine.
So the machine-facing contract of most CLIs was never actually designed. It accreted. A --json flag got added to the two commands someone needed to script, with a shape that matched those commands and no other. The exit code was zero on success and one on “something went wrong”, with no vocabulary in between. None of it was a decision so much as a run of local patches, and the result is an interface a human drives effortlessly and a machine drives by guessing.
The tax on the person doing the scripting
Day to day, the cost lands on whoever tries to build on top of the thing.
You script around a CLI that was not built to be scripted, so you parse output formatted for a person. You grep stderr for an error string, and it changes wording in the next release and your script quietly stops catching it. You awk a column out of a table, and it breaks the day a value grows long enough to shift the alignment. You cannot tell “succeeded” from “succeeded with warnings”, because both exit zero, so you either ignore warnings or reimplement the tool’s judgement by pattern-matching its prose. Every integration is a small pile of heuristics, and every upgrade is a chance for one of them to break without telling you.
The tell is that you are reverse-engineering an interface the tool could simply have stated. The information exists. It is just encoded for the wrong reader.
The tax on the people betting on automation
One level up, the thing at stake is reach, because automation is how a small team does the work of a larger one.
Agents and CI pipelines are now first-class operators of internal tooling. They publish, they lint, they roll credentials, they run the checks. If those tools cannot be driven deterministically, the boring high-volume work automation is supposed to absorb stays manual, and the ceiling on what a team can operate stays low. That is the visible cost.
The invisible cost is worse. A CLI that reports success when it half-succeeded does not merely fail to help; it turns automation into a liability. A human running that command once will catch the yellow warning. An agent running it a thousand times will ship the warning a thousand times, confidently, and the first you hear of it is downstream, as a problem that has already spread. The more you automate against a tool that cannot speak precisely to a machine, the more efficiently you propagate its ambiguities.
Exit codes are a contract, or they are noise
You can see the whole argument in the exit code. A single non-zero for “something went wrong” tells a caller only that it should probably stop. A stable, documented set of codes tells the caller what happened and what to do about it.
Consider the difference between parsing a sentence and reading a number. If the tool guarantees that three means “not authenticated”, four means “permission denied”, five means “not found”, and seven means “your input failed validation”, then an agent branches on the number: re-authenticate on three, escalate on four, stop cleanly on five, show the user their mistake on seven. No prose parsing, no version-fragile string matching, no guessing. The same holds for output: a structured object of a few hundred tokens that says exactly what happened is worth more to a machine than a beautiful table it has to interpret, and it costs the tool nothing to emit both.
flowchart TD
RUN[Agent runs the command] --> CODE{Exit code}
CODE -->|0| OK[Use the structured result]
CODE -->|3| AUTH[Re-authenticate, then retry]
CODE -->|4| PERM[Stop, escalate to a human]
CODE -->|5| NF[Stop cleanly, nothing to do]
CODE -->|7| VAL[Surface the input error to the user]
The catch is that this only works if the codes are a contract that never moves. The day someone repurposes a code, every agent built on it breaks silently, which is the one failure worse than breaking loudly.
Why the alternatives do not cover it
It is worth being clear about why the obvious substitutes do not close the gap.
Dashboards are built for the human eye, and an agent cannot use one in any real sense. Screen-driving tools exist, but pointing a model at pixels to click a button is slow, brittle, and exactly the wrong contract for a machine that wants structured facts. A GUI is the least machine-legible interface there is.
REST APIs are the opposite: excellent for machines, and not where humans work day to day. The CLI is the one surface both a person and an agent can use, which is precisely why its output contract matters so much. It is the shared door.
A --json flag bolted onto a human-first tool helps and does not finish the job. Coverage is usually partial, the shapes are inconsistent between commands, and the defaults are still human: colour, spinners, and interactive prompts that hang a non-interactive caller waiting for input that never comes. Machine-friendliness cannot be one flag on some commands. It has to be every default, flipped at once.
MCP and tool wrappers are a genuinely good direction, exposing operations as typed tools an agent can call. But a great many operational tasks are CLI-shaped, the CLI is the artifact engineers already have and trust, and underneath, tool wrappers frequently shell out to the CLI anyway, which means the CLI’s output contract is load-bearing whether or not there is an MCP layer on top of it.
What would have to be true
Argue it from the properties.
Machine output has to be a mode, not a flag. One switch, an environment variable or an explicit agent mode, should flip every default at once: structured output, no colour, no spinner, no update banner, no interactive prompt. A machine caller should never have to opt into machine behaviour one command at a time.
Exit codes have to be a documented, stable contract, one meaning per code, never repurposed. A caller should branch on the code and be right, this release and the next.
The tool has to describe itself. If it can emit a machine-readable manifest of its commands, their flags, their arguments and their exit codes, an agent can compose operations without a human hardcoding knowledge of the surface, and the description stays true because it comes from the tool rather than from documentation that drifts.
Errors have to be structured and on the right stream, so a caller can tell an error from a result without scraping. And all of it has to be consistent across every command, because the one thing an agent cannot do is learn the per-command exceptions a human absorbs without noticing.
graph TD
H[Human at a terminal]
A[Agent or CI pipeline]
CLI["One CLI, one command surface"]
H -->|default mode| CLI
A -->|agent mode| CLI
CLI -->|colour, spinner, a readable table| H
CLI -->|JSON, no colour, stable exit codes| A
How we come at it
Full disclosure: this is the spec0 blog, and our CLI is built on exactly these choices, so read this as interested rather than neutral.
The spec0 CLI treats the machine caller as a first-class user. An agent mode flips every default for non-human callers at once: structured JSON, no colour, no spinners, no update banner. The exit codes are a stable contract with a distinct meaning per case, so an agent branches on the code instead of parsing the prose. And the whole command surface, flags, arguments and exit codes included, is available as a machine-readable manifest, so an agent can discover what the tool can do rather than having it hardcoded.
None of that is exotic, and that is the point. It is what treating the machine as a real user looks like, and the reason it is worth naming is that most CLIs, including good ones, still treat it as an afterthought.
The part worth arguing about
The uncomfortable thing is that one CLI now serves two readers who want opposite treatment. The human wants warmth: colour, a spinner that says the tool is alive, a friendly one-line summary. The machine wants none of it, and a mode flag that strips it all away is a workaround, a way of running two interfaces out of one binary, not a real reconciliation.
So the question I keep coming back to is whether the end state is one command with two personalities, or whether the machine interface eventually wants to be something that is not a CLI at all, at which point the CLI goes back to being purely for people. And underneath that, a governance question with teeth: as the tool grows, who owns the exit-code contract, and what stops a well-meaning change from repurposing code five and breaking every agent downstream without a single error to show for it? If you have run a CLI that stayed honest to both readers as it grew, I would like to hear how you kept the contract still.