DEV Community

zhayujie
zhayujie

Posted on

Designing an agent-friendly CLI

As agents mature, more and more products will need to serve two kinds of users at once: humans and agents. A CLI is one of the easier forms for an agent to call, and in the future many products may want to ship their own CLI to expose their core capabilities to agents. This post walks through the design of a CLI we recently open-sourced for a platform product, covering six areas: language choice, login and authorization, command and flag design, skills, distribution, and security.

Why a CLI

An agent can reach an external capability in roughly three ways: calling an API directly, going through MCP, or running a command in a terminal. All three are ultimately wrappers over the same remote interfaces, but a CLI has two advantages for agents. First, almost every general-purpose agent already ships with a Bash tool (Codex, Claude Code, CowAgent, OpenClaw, and so on), so there is no MCP server for the provider to stand up. Second, the CLI packs auth, parameter assembly, pagination, and error handling into the command itself: the agent does not have to construct HTTP requests, and it does not need the full API reference in context — a short command description is enough to decide what to run, which saves tokens and reduces mistakes.

What changes is the shape of the user. A CLI used to be driven by a developer sitting at a terminal; now there is also an agent running a "read output, decide, run the next command" loop. The two have quite different needs, and serving both well becomes a design problem in itself. The rest of this post uses a real CLI as an example and walks through the key decisions along the way. The overall picture:

CLI design overview

1. Language

A few hard requirements shaped the language choice:

  • Single file, no runtime dependency: an agent's environment is unpredictable — a local machine, a Docker container, a remote Linux box — and you cannot assume the right Node or Python version is installed. Ideally you download one binary and it just runs.
  • Easy cross-compilation: it needs to cover macOS / Linux / Windows × amd64 / arm64 from a single codebase.
  • Fast startup: an agent calls commands frequently, so a compiled language is preferable to an interpreter that pays cold-start cost each time.

A quick comparison of the candidates:

  • Node / TypeScript: great ecosystem and fast to write, but it depends on a Node runtime on the target machine, and bundling to a single file produces large artifacts.
  • Python: same runtime and version headaches, and distribution is a real pain.
  • Rust: single binary, good performance, meets every requirement, but development and compile speed are slower.
  • Go: a single static binary, zero dependencies with CGO_ENABLED=0, solid cross-compilation, millisecond startup, and reasonable development speed.

We ended up choosing Go, with Cobra as the CLI framework for command parsing and routing. Because the artifact is a plain binary, a single GoReleaser config can produce the npm package, Homebrew cask, and GitHub release together. There is no universal answer here — it depends on your team's stack — but if agents are the primary users, a language that compiles to a zero-dependency single binary should come first. How smoothly you can distribute it directly decides whether an agent can install it on its own.

2. Login and Authorization

The simplest way to authorize a CLI is to have the user paste an API key. The problems are obvious: an API key is usually long-lived, carries full permissions, and sits in plaintext on disk. If it leaks, everything is exposed, and there is no way to scope it per operation. For a CLI that agents call automatically and that manages real resources, that "one key opens every door" model is too risky.

So we use the OAuth 2.0 Device Authorization Grant (device flow). The user logs in and grants scopes in a browser, and the CLI receives a short-lived, refreshable, scope-limited access token that the server can revoke at any time. The flow has three steps:

Device flow diagram

There is another common OAuth variant where the CLI starts a temporary local HTTP server to catch the browser redirect (tools like gh and gcloud do this). That does not work well for agents, which often run on a server with no browser, no display, and no open ports. The device flow lets the authorization happen in a browser on any machine, while the CLI side only kicks it off and polls. Two parts of this are worth calling out.

2.1 Two-phase polling

When a human logs in, the CLI can open the browser and block on polling until the user is done. But an agent runs a "run a command, read the result, decide the next step" loop. If a single tool call both prints the login link and then blocks on polling, the link never makes it back to the user through the model, and the whole session stalls.

So the agent login is split into two phases. The first phase returns immediately. The JSON it returns includes the verification URL, the device code, and a next_action field that tells the agent exactly what to run next:

linkai auth login --no-wait --json
Enter fullscreen mode Exit fullscreen mode

The second phase polls, blocking for at most a bounded number of seconds before returning. If the user has not finished, the agent simply runs the same command again on its next tool call:

linkai auth login --device-code <code> --wait 60 --json
Enter fullscreen mode Exit fullscreen mode

The --wait flag distinguishes the two users: leaving it off (blocking until the device code expires) gives the interactive human flow; passing --wait switches to the bounded polling path an agent needs.

2.2 The authorization page

The authorization page is where the user actually interacts and makes a decision, so it needs to make three things clear:

  • which account / device / CLI is requesting authorization
  • which permissions are being requested, listing the scopes per module so the user can see exactly what the CLI is being granted
  • a note that they can return to the terminal once done, since the terminal is still polling

CLI authorization page

3. Commands and Flags

For the same command, a developer wants a clean table and streaming output, while an agent wants a single parseable, structured response. A few details follow from that.

CLI terminal output

3.1 JSON output

--json is a global flag that makes any command emit structured JSON. With it, the agent's skill only has to say "always pass --json," and every command returns output the agent can parse reliably.

3.2 Streaming vs. non-streaming

For commands that talk to a model, humans like the streaming (SSE) typewriter effect. But when an agent calls the command through Bash, the output gets piped, and streaming breaks the reply into fragments that are hard to parse — the agent wants the full reply in one piece.

Rather than forcing the user to remember --stream / --no-stream, the default follows the environment: streaming in a terminal, non-streaming when the output is piped or redirected (the typical agent case), and always non-streaming with --json. An explicit flag overrides all of this.

3.3 Dry run for writes

Destructive or mutating commands support --dry-run, which prints the request that would be sent instead of actually sending it. The agent can check the parameters before it commits to the operation, which also adds a layer of safety.

3.4 Output separation and exit codes

Results go to stdout; everything procedural goes to stderr. Progress notes, update notices, and confirmation prompts are all things that, if mixed into stdout, would break the agent's JSON parsing. With them on stderr, the agent only has to read stdout.

Exit codes are also structured so the agent can decide whether to retry or stop: 0 success, 1 generic error, 2 bad arguments, 3 auth/permission, 4 network. On a permission error (exit 3), the message includes the command to fix it (for example linkai auth login --scope "..."), so the agent re-triggers authorization instead of retrying blindly.

4. Skill Design

The CLI wraps a set of interfaces in commands; the next problem is helping the agent understand how to call them correctly. Probing with --help over and over is too costly, so the CLI ships a companion skill — a manual written for the agent.

The skill structure is kept as flat as possible: a single SKILL.md as the entry point, with per-module command details under a references/ directory.

skills/linkai-cli/
├── SKILL.md          # entry point: global notes + module overview + decision flow
└── references/       # details per module: auth / install / admin ...
Enter fullscreen mode Exit fullscreen mode

Installed into an agent, this is a single directory. The idea is to have the agent read the main skill first and only dig into references/ when it needs module-level detail. Some CLIs install every sub-module as its own separate skill, which is not ideal: the agent loses a global view of the CLI, and a large number of sub-skills inflates its context.

The skill is embedded into the binary with go:embed and locked to the CLI version, so the docs never drift from what the binary actually supports. A skill install command handles installation in one step.

One more detail: the main SKILL.md includes a short install section. When an agent gets the skill first — for example by downloading it directly — it can still follow those instructions to install the CLI binary.

5. Distribution and Updates

Distribution matters a lot for usability, and there are two things to distribute: the CLI binary and the agent skill. The goal is that one sentence to an agent is enough for it to get set up. It helps to support several channels so developers and agents on any platform can find one that works:

Method Command
npm npm i -g linkai-cli
install script curl -fsSL .../install.sh
Homebrew brew install .../linkai
Go go install .../linkai-cli@latest
GitHub Release download the binary

On a machine with Node, npm is the most convenient path: it hides the OS and CPU differences, picks the right binary, and puts it on PATH. The install script is a better fit for dependency-free environments; besides downloading the CLI, it also drops the skill into the skill directories of common agents (Codex, Claude Code, Cursor, OpenClaw, CowAgent, and others).

To let an agent set everything up from one sentence, a clear install guide (install.md) matters too. It writes out the full from-scratch steps, so the user can just hand the agent a line like:

Read https://cdn.link-ai.tech/cli/install.md and follow it to install the CLI and skill, then start using it.

Installing the CLI inside an agent

Updates are an extension of distribution, done in two layers:

  • Passive notice: on startup the CLI fetches and caches the latest version in the background, and prints a one-line notice to stderr on exit (mainly for the human at the terminal, so it never pollutes the stdout an agent parses).
  • Active update: update detects how the CLI was originally installed, calls the matching package manager to upgrade, and syncs the skill afterward.

6. Security

Login and authorization are only the first layer. A few more things matter in the agent case:

  1. Least privilege. Scopes use a resource:action format (for example app:read, db:write). By default only read and content-generation scopes are granted; mutating and destructive operations must be requested explicitly. Each command declares the scope it needs and it is checked centrally, so even if an agent slips, it cannot go beyond what the user granted.

  2. Token storage. On macOS the credential goes into the system keychain; on other platforms it is stored in a file (mode 0600). The token is an opaque, server-revocable token rather than a self-decoding JWT, and logout revokes it on the server.

  3. Device binding. Every request carries a device ID, and the server binds the token to that device, which limits abuse if a token leaks.

  4. Dangerous-character filtering. Content an agent handles may come from untrusted sources (web pages, external input) and can carry invisible attack characters — Bidi overrides, zero-width characters, ANSI escapes — that alter command meaning or spoof the terminal. The CLI rejects these on input and strips them on output.

Wrapping Up

The way you design a CLI is quite different from designing a GUI product, and even among CLIs, one for agents differs from one for developers. To summarize the main ideas:

  • Language: prefer a compiled language that produces a single binary.
  • Login: use the device flow so authorization happens in a browser, and support phased login for agents.
  • Commands and flags: give humans a clean view and agents structured data, tolerate extra flags, allow dry runs, and return next-step hints on error.
  • Skill: as a manual for the agent, keep it a single directory with a flat structure to lower both the learning and context cost.
  • Distribution: support multiple channels and add an install guide an agent can follow on its own.
  • Security: beyond login, cover least privilege, credential protection, device binding, and filtering of untrusted content.

None of this is specific to CLIs. Structured output, tolerant error handling, a companion skill, smooth distribution, and least privilege apply just as well to APIs, SDKs, and other products that serve both humans and agents.

Open-source project referenced in this post: github.com/MinimalFuture/linkai-cli

Top comments (0)