---
name: agent-friendly-cli
source: https://app.decimal.ai/s/agent-friendly-cli@1/SKILL.md
source_sha256: 18ad3cfc6d96
---

# Agent-Friendly CLI conventions

## Contract

When you write or modify a command-line tool that an AI agent will invoke
non-interactively, enforce the exact output, error, exit-code, flag, and safety
conventions below. They are deliberate, arbitrary choices (specific flag spellings,
specific exit numbers, a specific error shape) — not general "good CLI" advice — and a
model that does not know them will guess differently every time.

## Rules

### R1 — JSON is the no-flag default

- The default output (no flags at all) is a single JSON value written to **stdout**.
- Do **not** require `--json`, `--format=json`, `--output=json`, or any flag to get
  JSON. The agent must get machine-readable output by typing nothing extra.
- The JSON must be parseable by `jq .` — one well-formed value, no leading log lines,
  no human prose mixed in.

### R2 — `--human` is the (only) name for human output

- Provide exactly `--human` to switch to colored / tabular / formatted output for a
  person. Not `--pretty`, `--color`, `--text`, `--table`, `--format=text`.
- Optionally provide `--agent` as an explicit alias for the JSON default (used when an
  env var or config has flipped the default to human).

### R3 — stdout is data only; stderr is everything else

- **stdout** carries the data payload and nothing else — no logs, no progress bars, no
  banners, no warnings. This is what makes the tool pipe-safe.
- **stderr** carries logs, progress, diagnostics, and the error object (see R4).

### R4 — Errors are a JSON object on stderr with exactly four keys

On any failure, write this object to **stderr** (never stdout):

```json
{"error": true, "code": "MACHINE_CODE", "message": "human readable", "suggestion": "concrete next action"}
```

- `error` — the boolean `true`.
- `code` — a machine-readable token in `SCREAMING_SNAKE_CASE`, e.g. `MISSING_REQUIRED`,
  `AUTH_EXPIRED`, `NOT_FOUND`, `CONFLICT`. It is an API contract: never rename a code
  across versions.
- `message` — a human-readable sentence describing what went wrong.
- `suggestion` — a concrete next command or action the caller can take. Always present,
  never empty.
- Never drop into an interactive prompt on error — write the object and exit immediately.

### R5 — Exit codes use this exact table

| Code | Meaning |
|------|---------|
| `0`  | success |
| `1`  | general / unexpected error |
| `2`  | parameter or usage error (unknown flag, missing required flag, type mismatch) |
| `10` | authentication failed |
| `11` | permission denied |
| `20` | resource not found |
| `30` | conflict / precondition failed |

- Never exit `0` and then report an error in the output. A failure is always non-zero.
- Pick the most specific code: a missing record is `20`, not `1`; bad credentials are
  `10`, not `2`.

### R6 — Input is validated, never prompted

- A missing required argument produces the R4 error object and exit `2` — never an
  interactive "Enter name:" prompt.
- A type mismatch (e.g. `--count abc` where an integer is expected) produces the R4
  error object and exit `2`.
- An unknown / unrecognized flag is rejected with the R4 error and exit `2`; it is never
  silently ignored.

### R7 — Destructive operations require `--yes`

- Any operation that deletes, drops, or overwrites requires an explicit `--yes` flag to
  proceed. Not `--force`, not `-f`, not an interactive `y/n` prompt.
- Provide `--dry-run` to preview what a destructive command would do without doing it.

### R8 — Reserved flag names

These flag spellings are reserved and mean exactly this — do not repurpose them:

| Flag | Meaning |
|------|---------|
| `--human` | switch to human-readable output |
| `--agent` | explicit JSON output (the default) |
| `--yes` | confirm a destructive operation |
| `--dry-run` | preview without executing |
| `--quiet` | suppress non-data (stderr) output |
| `--brief` | print a one-paragraph identity of the tool |
| `--version` | print a semver version string |

## Worked examples

### R1 — JSON by default (BEFORE → AFTER)

BEFORE (base default: human table, JSON behind a flag):

```
$ tasks list
ID  TITLE      STATUS
1   Buy milk   todo
# (JSON only with: tasks list --json)
```

AFTER (conforming: JSON is the no-flag default):

```
$ tasks list
{"result": [{"id": 1, "title": "Buy milk", "status": "todo"}]}
$ tasks list --human
ID  TITLE      STATUS
1   Buy milk   todo
```

### R2 — flag name (BEFORE → AFTER)

BEFORE: `tasks list --pretty` for colored output.
AFTER: `tasks list --human` for colored output (`--pretty` is not the convention).

### R3 — stream separation (BEFORE → AFTER)

BEFORE (progress on stdout corrupts the pipe):

```
$ download report.csv | jq .
Downloading... 42%        <- pollutes stdout, jq chokes
{"path": "report.csv", "bytes": 10240}
```

AFTER (progress on stderr, data on stdout):

```
$ download report.csv 2>/dev/null | jq .
{"path": "report.csv", "bytes": 10240}
# "Downloading... 42%" was written to stderr
```

### R4 — error shape (BEFORE → AFTER)

BEFORE (prose to stdout, exit 0):

```
$ deploy --env prod
Error: your access token expired 2 hours ago, please log in again
$ echo $?
0
```

AFTER (four-key JSON to stderr, specific exit code):

```
$ deploy --env prod
# (stderr:)
{"error": true, "code": "AUTH_EXPIRED", "message": "Access token expired 2 hours ago",
 "suggestion": "Run 'deploy auth refresh' to get a new token"}
$ echo $?
10
```

### R5 — exit codes (BEFORE → AFTER)

BEFORE: every failure exits `1`.
AFTER: missing record → `20`; bad credentials → `10`; permission denied → `11`;
version conflict → `30`; bad flag → `2`; truly unexpected → `1`.

### R6 — missing required argument (BEFORE → AFTER)

BEFORE:

```
$ greet
Enter a name: ▌          <- interactive prompt, hangs the agent
```

AFTER:

```
$ greet
# (stderr:)
{"error": true, "code": "MISSING_REQUIRED", "message": "--name is required",
 "suggestion": "Pass --name <value>, e.g. greet --name Ada"}
$ echo $?
2
```

### R7 — destructive confirmation (BEFORE → AFTER)

BEFORE:

```
$ project delete 42
Are you sure? [y/N] ▌     <- interactive, or executes with --force
```

AFTER:

```
$ project delete 42
{"error": true, "code": "CONFIRMATION_REQUIRED", "message": "Refusing to delete without confirmation",
 "suggestion": "Re-run with --yes, or preview with --dry-run"}
$ project delete 42 --yes
{"result": {"deleted": 42}}
```

## Edge cases & exceptions

- **Empty result is still JSON.** A command that finds nothing prints `{"result": []}`
  on stdout with exit `0` — not an empty body and not an error.
- **Warnings are not errors.** A non-fatal warning goes to stderr as a log line (or a
  `{"warning": ...}` object on stderr if structured), the data still goes to stdout, and
  the exit code stays `0`.
- **`--quiet` silences stderr, not the error object.** Even under `--quiet`, a failure
  still emits the R4 error JSON to stderr and exits non-zero; `--quiet` only suppresses
  routine logs/progress.
- **`--dry-run` of a destructive op exits `0`** and reports what *would* happen on
  stdout; it does not require `--yes` because nothing is destroyed.
- **Auth vs. permission.** Bad/expired credentials are `10` (AUTH_*); valid identity but
  insufficient rights is `11` (PERMISSION_DENIED). Do not collapse both into one.
- **Not-found vs. conflict.** Reading a missing id is `20`; writing against a stale
  version / violated precondition is `30`. Choose by which actually occurred.
- **`--help` and `--version`** may print to stdout and exit `0`; they are the documented
  exception to "stdout is data only" because their output *is* the requested data.

## Do / Don't

- Never gate JSON behind `--json`; always make JSON the no-flag default.
- Never name the human flag `--pretty`/`--color`/`--text`; always name it `--human`.
- Never write logs or progress to stdout; always send them to stderr.
- Never report an error on stdout with exit `0`; always use stderr + a non-zero code.
- Never confirm destructive ops interactively or with `--force`; always require `--yes`.
- Never invent ad-hoc exit numbers; always use the R5 table.
- Never omit `suggestion` from an error; always include a concrete next action.
- Never rename a `code` token across versions; always treat codes as a stable contract.

## Common mistakes

- Defaulting to a pretty table and hiding JSON behind `--json` (backwards for agents).
- Emitting the error as a plain string instead of the four-key object.
- Putting the error on stdout, which makes a downstream `jq` parse garbage.
- Returning exit `0` on failure because the program "handled" the error.
- Using `1` for everything instead of the specific `2/10/11/20/30` codes.
- Prompting for a missing argument instead of failing fast with exit `2`.
- Accepting an unknown flag silently instead of rejecting it with exit `2`.
- Spelling the confirmation flag `--force` or relying on a `y/N` prompt.

## Quick checklist

- [ ] No-flag output is `jq`-parseable JSON on stdout.
- [ ] `--human` switches to formatted output.
- [ ] stdout = data only; logs/progress on stderr.
- [ ] Errors = `{error, code, message, suggestion}` JSON on stderr.
- [ ] `code` is `SCREAMING_SNAKE_CASE`; `suggestion` is always present.
- [ ] Exit codes follow `0/1/2/10/11/20/30`; failures never exit `0`.
- [ ] Missing/typed/unknown flags → exit `2`, structured error, no prompt.
- [ ] Destructive ops require `--yes`; `--dry-run` previews.
