Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when building or modifying a CLI for AI agents to call: emit JSON by default, structured error JSON to stderr, and the specific exit-code table below.
.claude/skills/agent-friendly-cli/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 100% | 4 |
| Model | Lift | Δ tokens | Δ turns | Cases | Verified |
|---|---|---|---|---|---|
| gemini-3.5-flashbest | +75% | — | 0% | 24 | 88d ago |
| gemini-3.6-flash | +59% | +82% | 0% | 22 | 56d ago |
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-19 | ✗→✓ | ▲ Improved | — | — |
| case-22 | ✗→✓ | ▲ Improved | — | — |
| case-20 | ✗→✓ | ▲ Improved | — | — |
| case-03 | ✗→✓ | ▲ Improved | — | — |
| case-02 | ✗→✓ | ▲ Improved | — | — |
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.
--json, --format=json, --output=json, or any flag to getJSON. The agent must get machine-readable output by typing nothing extra.
jq . — one well-formed value, no leading log lines,no human prose mixed in.
--human is the (only) name for human output--human to switch to colored / tabular / formatted output for aperson. Not --pretty, --color, --text, --table, --format=text.
--agent as an explicit alias for the JSON default (used when anenv var or config has flipped the default to human).
banners, no warnings. This is what makes the tool pipe-safe.
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.
| 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 |
0 and then report an error in the output. A failure is always non-zero.20, not 1; bad credentials are10, not 2.
2 — never aninteractive "Enter name:" prompt.
--count abc where an integer is expected) produces the R4error object and exit 2.
2; it is neversilently ignored.
--yes--yes flag toproceed. Not --force, not -f, not an interactive y/n prompt.
--dry-run to preview what a destructive command would do without doing it.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 |
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 todoBEFORE: tasks list --pretty for colored output. AFTER: tasks list --human for colored output (--pretty is not the convention).
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 stderrBEFORE (prose to stdout, exit 0):
$ deploy --env prod
Error: your access token expired 2 hours ago, please log in again
$ echo $?
0AFTER (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 $?
10BEFORE: every failure exits 1. AFTER: missing record → 20; bad credentials → 10; permission denied → 11; version conflict → 30; bad flag → 2; truly unexpected → 1.
BEFORE:
$ greet
Enter a name: ▌ <- interactive prompt, hangs the agentAFTER:
$ greet
# (stderr:)
{"error": true, "code": "MISSING_REQUIRED", "message": "--name is required",
"suggestion": "Pass --name <value>, e.g. greet --name Ada"}
$ echo $?
2BEFORE:
$ project delete 42
Are you sure? [y/N] ▌ <- interactive, or executes with --forceAFTER:
$ 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}}{"result": []}on stdout with exit 0 — not an empty body and not an error.
{"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 failurestill 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 onstdout; it does not require --yes because nothing is destroyed.
10 (AUTH_); valid identity butinsufficient rights is 11 (PERMISSION_DENIED). Do not collapse both into one.
20; writing against a staleversion / violated precondition is 30. Choose by which actually occurred.
--help and --version may print to stdout and exit 0; they are the documentedexception to "stdout is data only" because their output is the requested data.
--json; always make JSON the no-flag default.--pretty/--color/--text; always name it --human.0; always use stderr + a non-zero code.--force; always require --yes.suggestion from an error; always include a concrete next action.code token across versions; always treat codes as a stable contract.--json (backwards for agents).jq parse garbage.0 on failure because the program "handled" the error.1 for everything instead of the specific 2/10/11/20/30 codes.2.2.--force or relying on a y/N prompt.jq-parseable JSON on stdout.--human switches to formatted output.{error, code, message, suggestion} JSON on stderr.code is SCREAMING_SNAKE_CASE; suggestion is always present.0/1/2/10/11/20/30; failures never exit 0.2, structured error, no prompt.--yes; --dry-run previews.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-16 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.5-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +59 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.5-flash | verified | 6/26/2026 | +75% |
Other measured skills in the registry, with their headline benchmark lift.