TLDR: The fastest-growing user of your command-line tool is an agent, not a human. Agent-native means: headless by default, JSON on stdout, actionable errors on stderr, honest exit codes. On the format side, there are three layers that want opposite things — markdown for instructions, JSON for the wire, free-form for generation. Most of this is just the Unix philosophy, finally enforced.
Pattern: CLI as the Native Agent Surface
A CLI is text in, text out, composable, with exit codes and
--help. That contract was designed in 1978 for humans at a
terminal. It turns out to be the contract agents need too.
Scalekit ran 75 comparative tasks against GitHub: gh CLI
versus the Copilot MCP server, with the same prompts and model. The CLI
used 200–9,000 tokens per task; MCP used 32,000–82,000. At 10k monthly
operations, that was $3.20 versus $55.20. Reliability was 100% for the
CLI and 72% for MCP, mainly due to timeouts (benchmark
write-up).
There is a deeper reason beyond tokens and reliability: models have
read the history of the terminal. git, curl,
jq, and ffmpeg are in the weights. A bespoke
tool schema is not, and its context cost recurs on every turn.
The ecosystem converges on a filesystem, executables, and stdout. That is a shell.
Pattern: Failure-Driven Interface Design
This pattern grounds interface design in the self-teaching
error surface: failure modes observed across hundreds of real agent
trajectories (Claude, Copilot, Codex, agent_probe) captured in a local
trajectory corpus and searched with recoll-traj. The
failures are boringly repetitive: TUI traps, silent successful exits,
retry loops caused by unhelpful errors, ANSI-decorated output, and
invisible stdin prompts.
An audit of eight local scripts against this checklist found that
seven had no --json, six mishandled error prefixes or
streams, and five had no real --help. The rules are easy
and nobody follows them, including their authors.
Pattern: Headless by Default
Failure mode: An agent passes
--prompt "do X" expecting headless execution. The CLI
interprets this as seeding a TUI, opens an interactive session, and the
agent process hangs until the timeout fires.
Observed example: opencode --prompt "..." seeds
the TUI; the correct headless invocation is
opencode run [message]. A benchmark harness had to detect
which invocation mode each CLI supported by parsing --help
output — a fragile heuristic that broke for
deepseek-v4-flash because the flag detection returned wrong
results, causing all tasks to time out.
Rules: - Provide a flag (e.g.,
--non-interactive, --headless,
-p, or a subcommand like run) that is
unambiguous and fully bypasses any TUI or interactive flow. - Document
this flag prominently in --help, with a one-line example. -
When stdin is not a TTY and no flag was given, default to
non-interactive mode and do not block. - Keep the non-interactive entry
point stable across versions; changing it forces all calling harnesses
to update their detection logic.
Pattern: Honest Exit Status
Failure mode: A command succeeds silently (empty stdout, exit 0) or fails silently (error swallowed, still exit 0). The agent cannot distinguish between “completed normally,” “did nothing,” and “crashed quietly.”
Observed example: Running
python /tmp/fib_demo.py returned empty stdout with exit
code 0 — the agent couldn’t tell whether it succeeded or not. The
workaround documented in improved system prompts: append
; echo "EXIT: $?" to every command to extract the exit code
explicitly.
Rules: - Exit 0 means success. Exit non-zero means
failure. Never exit 0 after printing an error message. - Provide named
or documented exit codes for distinct failure categories (e.g., 1 = user
error, 2 = configuration error, 3 = network failure). Even a simple
two-value convention (0 / non-zero) beats an undocumented mix. - On
success, print at least one line of confirmation. A silent success is
indistinguishable from a silent failure. - Propagate exit codes through
wrappers; do not swallow a subprocess’s non-zero exit with an
unconditional exit 0.
Pattern: Actionable, Singular Errors
Failure mode: An agent reads an error like
Permission denied or No such file or directory
and enters a guess-and-retry loop, often repeating the same failing
invocation multiple times.
Observed examples: - 139 matches for
permission denied in the corpus, many showing an agent
retrying the identical failing SSH or config-file access 5–10 times. -
75 matches for no such file or directory, including agents
assuming standard filenames (pyproject.toml,
setup.py, package.json) exist without
checking. - unrecognized arguments appeared 31 times, often
after an agent hallucinated a flag (--yes,
--lines 40, --assign-identity) not present in
the actual CLI.
Rules: - Print the full path that was not found, not
just the filename. - For permission errors, state what permission is
required and, if recoverable, how to fix it (e.g., “Run as root” or
“Check that your SSH key is loaded with ssh-add”). - For
unrecognized flags, suggest the closest valid flag or print the relevant
section of help. - Do not combine errors. If three things are wrong,
emit three separate, numbered error lines. - Prefix error output with
ERROR: or error: consistently so agents can
detect it by string matching. Avoid printing errors to stdout; use
stderr.
Pattern: Machine-Readable Results
Failure mode: The agent’s downstream code tries to parse human-readable tabular or prose output. Column alignment, header lines, pluralization, and unit suffixes all vary by terminal width or locale, making regex-based parsing fragile.
Observed example: ANSI color codes embedded in stdout
(\x1b[32m, etc.) broke agents trying to extract fields by
splitting on whitespace. A --no-color flag was added as a
workaround, but the agent still had to strip header lines and unit
strings.
Rules: - Emit machine-readable JSON. Either make
JSON the default output (recommened) or provide a --json /
--output json flag. - If JSON is the default, say so
explicitly in --help and show an example of the output
schema. - If a human-readable default is kept, suppress all decorative
output (spinners, progress bars, color, banners) when
--json is active. - Keep the JSON schema
backwards-compatible across minor versions; add fields, never remove or
rename them. - At minimum, provide --no-color /
NO_COLOR env-var support (the standard from
no-color.org).
Pattern: Explicit Input Channels
Failure mode: A CLI reads from stdin without being
told to. When invoked by an agent as a subprocess with stdin connected
to /dev/null or a pipe, it blocks indefinitely waiting for
input that never arrives.
Observed examples: GPG’s interactive passphrase prompt;
ssh waiting for a host-key confirmation; package managers
asking “Continue? [y/N]” — all caused agent hangs that only resolved on
timeout.
Rules: - Only read from stdin when the user
explicitly passes - as an argument or invokes an explicitly
interactive subcommand. - If stdin is not a TTY and the CLI would
otherwise prompt, fail immediately with a clear error:
error: interactive input required but stdin is not a terminal; use --yes or --passphrase=FILE.
- Provide --yes / --assume-yes /
--force flags for operations that would otherwise prompt
for confirmation. - For credentials, accept
--password-file, --token-file, or environment
variables so agents can inject secrets without opening a pipe to
stdin.
Pattern: Explicit Project Roots
Failure mode: An agent is launched from a directory
where the CLI’s assumptions don’t hold (e.g., started from
$HOME instead of the project root). The CLI silently finds
no config file and proceeds with defaults, producing subtly wrong
results.
Observed example: A benchmark runner added a guard that
exits with an error when the working directory is $HOME,
because agents launched from the wrong directory would silently use
global defaults. File lookups for pyproject.toml,
setup.py, and package.json failed with
No such file or directory in 75 recorded steps, all caused
by wrong working directory.
Rules: - State the expected working directory in
--help (e.g., “Must be run from the repository root”). -
Detect and fail explicitly when a required file or directory does not
exist:
error: pyproject.toml not found in current directory; run from the project root.
- Never silently fall back to a home-directory config when a
project-level config was expected. - Provide a
--project-dir or --root flag so callers can
set the working context without cd.
Pattern: Stable, Discoverable Interface
Failure mode: An agent probes --help to
detect which flags are available. If the help text is verbose prose,
uses inconsistent formatting, or omits flags, the agent may hallucinate
flags based on training data, leading to
unrecognized arguments errors.
Observed examples: - Agents guessed -n,
--lines 40, --base-url,
--key-name, --assign-identity — none of which
existed in the target CLI. - A benchmark harness detected the
non-interactive flag by scanning --help output for
--non-interactive or --prompt; this broke for
CLIs where the flag was documented differently.
Rules: - List every flag on its own line, in the
form --flag-name VALUE Description. - Include at least
one usage example per subcommand. - Document the
non-interactive/headless flag explicitly with an example invocation. -
Keep --help output stable: same flags, same order, same
phrasing across patch versions. Agents may cache the output between
runs. - Emit --help to stdout (not stderr) so harnesses can
capture it with standard redirection.
Pattern: Bounded Execution
Failure mode: A CLI operation hangs indefinitely (network call, subprocess, lock contention). The outer agent times out the whole task, losing all context about the individual command’s progress.
Observed example: 97.3–97.5% of API calls in one session timed out due to backend degradation. The agent’s only signal was wall-clock timeout on the outer process; it had no way to know whether individual calls had partially succeeded.
Rules: - Set sensible internal timeouts for all I/O
operations (network, subprocesses, file locks). - Expose a
--timeout SECONDS flag so callers can tune per-call limits.
- On timeout, emit
error: operation timed out after 30s (use --timeout to adjust)
and exit non-zero. - For long-running commands, print progress
periodically (e.g., [3/10] processing...) so agents can
distinguish “slow but working” from “silently hung.” - Never let a
background thread or daemon prevent clean process exit; respect
SIGTERM promptly.
Pattern: Native Failure Signaling
Failure mode (API-level): A CLI that wraps an LLM
API returns an error string as a successful tool result. The model sees
"ERROR: invalid argument" as the tool’s output and — not
knowing the call failed — keeps retrying with the same bad arguments,
producing an infinite loop.
Observed example: claude-haiku-query.py built
tool_result blocks without "is_error": True
when the tool returned an error. The Anthropic API treats such blocks as
successful results; the model never learned to fix its call, causing an
infinite retry loop. The fix detects the ERROR: prefix and
sets "is_error": True.
Rules: - When your CLI is used as a tool in an
agentic framework, signal failure through the framework’s error channel,
not just through text content. - With the Anthropic Messages API, set
"is_error": true on tool_result blocks that
contain error content. - For OpenAI-compatible APIs, return an error
object in the tool response. Do not stuff error text into
content. - Use consistent error prefixes
(ERROR:, error:) so harnesses can detect
failures even when they cannot inspect the exit code.
Pattern: Safe Mutation Preview
Failure mode: An agent runs a command that changes files, cloud state, credentials, or databases before it has enough context to know whether the operation is safe. If the command has no preview mode, the agent can only choose between doing nothing and making an irreversible change.
Rules: - Provide --dry-run for every
command that mutates external state or writes outside the current
working tree. - Make dry-run output specific: list the files, resources,
records, or services that would change. - Use the same validation path
for dry runs and real runs so dry runs catch missing credentials,
invalid flags, and permission errors. - In JSON mode, include an
explicit "would_change": true|false field and a list of
planned operations. - Never perform hidden writes, migrations, network
calls with side effects, or credential updates during a dry run.
Pattern: Retry-Safe Mutations
Failure mode: A command partially succeeds, then times out or crashes. The agent retries the same command and creates duplicate resources, repeats an expensive operation, or corrupts state because the CLI cannot distinguish “already done” from “needs doing.”
Rules: - Design create/update/delete operations to
be safe to retry when possible. - Provide idempotency keys for remote
operations: --idempotency-key KEY. - If an operation has
multiple phases, persist enough state to resume or roll back cleanly. -
On partial failure, report exactly which steps completed and which did
not. - Treat “already exists in the requested state” as success unless
the user explicitly asks for strict creation semantics.
Pattern: Clean Result Channel
Failure mode: A CLI prints progress bars, warnings, debug logs, update notices, or telemetry messages into stdout. The agent’s parser receives a mix of data and commentary, then fails or extracts the wrong value.
Rules: - Reserve stdout for the command’s primary
result. - Send diagnostics, warnings, progress, and logs to stderr. -
When JSON output is active, stdout must contain only valid JSON or
newline-delimited JSON. - Provide --quiet to suppress
non-essential diagnostics and --verbose /
--debug to opt into more detail. - Disable update checks,
telemetry notices, and “new version available” banners in
non-interactive mode unless explicitly requested.
Pattern: Explainable Configuration
Failure mode: An agent changes an environment variable or config file but the CLI silently reads a different source. The agent then keeps editing the wrong file or passes duplicate flags trying to override hidden defaults.
Rules: - Document configuration precedence: flags,
environment variables, project config, user config, built-in defaults. -
Provide a command such as config explain or
--show-config that prints each effective setting and where
it came from. - Include config file paths in errors and diagnostics. -
In non-interactive mode, do not create or modify user-level config
unless the command explicitly says it will. - Avoid implicit global
state for project-scoped commands; prefer explicit
--config, --project-dir, or environment
variables.
Pattern: Visible Concurrency Control
Failure mode: Multiple agent commands run in parallel against the same files or remote resources. One command waits on a hidden lock, overwrites another command’s output, or fails with a vague conflict error.
Rules: - Use lock files or remote leases for
operations that cannot safely run concurrently. - When waiting on a
lock, print who holds it, what resource is locked, and how long the
command will wait. - Expose --lock-timeout SECONDS for
callers that need bounded execution. - Use atomic writes for generated
files. Write to a temporary file, then rename. - For conflict errors,
include the conflicting resource identifier and the safest recovery
command.
Pattern: Defensive Agent Boundary
Other projects converge on the same checklist. agentnative-cli is a linter over eight principles: non-interactive by default, structured output, progressive help, actionable errors, safe retries, composable structure, bounded responses, and a discoverable skill bundle. The Agent-Friendly CLI Spec extends this to 98 verifiable rules across three certification levels. CLI-Anything auto-generates agent-native CLIs for GUI applications such as GIMP, Blender, QGIS, and LibreOffice.
The principle people skip is that the agent is untrusted. Treat it as a public API client with a plausible-sounding hallucination generator attached. Validate its input accordingly, fail closed, and make safe recovery explicit.