Designing CLIs for AI Agents: The --json Pattern
AI agents are the new power users. Here's how to build CLIs they can actually use.
The best CLI users in 2026 aren't humans. They're AI agents.
Claude Code, Cursor, Copilot agents — they all shell out to CLI tools. And they all hit the same wall: parsing human-readable output.
✓ Server "my-app" forged (65.21.123.45)
SSH: gibil ssh my-app
TTL: 60 minutesA human reads that fine. An agent needs to regex out the IP address and hope the format doesn't change next release.
The fix: --json everywhere
Every command in gibil supports --json. The same create command:
{
"name": "my-app",
"ip": "65.21.123.45",
"ssh_command": "gibil ssh my-app",
"ttl_minutes": 60,
"expires_at": "2026-03-28T11:30:00Z"
}The agent calls gibil create --name my-app --json, parses the JSON, extracts ip, and moves on. No regex. No fragile string matching. No breaking when you add a field to the human output.
The pattern
If you're building a CLI that agents will use, here are the rules we follow:
1. Structured output on every command
Not just the important ones. Every command — create, list, destroy, run. An agent doesn't know which commands are "important."
# Human mode (default)
gibil list
# my-app 65.21.123.45 running 45m remaining
# Agent mode
gibil list --json
# [{"name": "my-app", "ip": "65.21.123.45", "status": "running", "ttl_remaining_minutes": 45}]2. Meaningful exit codes
0 for success. Non-zero for failure. Agents check exit codes before they parse output.
gibil run my-app "pnpm test" --json
# exit code 0 → tests passed
# exit code 1 → tests failed (output has details)
# exit code 2 → connection failed (different kind of error)Don't exit 0 when the operation failed but the CLI process "ran successfully." The agent needs the truth.
3. Stderr for logs, stdout for data
When --json is set:
- stdout: Only the JSON result
- stderr: Progress messages, warnings, debug info
# stdout (agent parses this)
{"name": "my-app", "ip": "65.21.123.45"}
# stderr (agent ignores this)
⠋ Forging server...
⠙ Waiting for SSH...
✓ ReadyAgents pipe stdout to jq without garbage. Humans still see progress in the terminal.
4. Idempotent commands
Agents retry. Networks fail. Commands get interrupted. If gibil destroy my-app is called twice, the second call should succeed (or return a clear status), not crash.
{"destroyed": true, "name": "my-app", "was_already_destroyed": false}5. Error objects, not error strings
When something fails:
{
"error": true,
"code": "SERVER_NOT_FOUND",
"message": "Server 'my-app' not found. Run gibil list to see active servers."
}The agent switches on code. A human reads message. Nobody parses an error string to figure out what went wrong.
Why this matters now
AI agents are moving from "answer a question" to "complete a task" — and tasks require tools.
The tools that work are the ones agents can use without guessing. --json, meaningful exit codes, clean error objects. None of this is hard to implement. It just needs to be intentional from the start.
Every CLI shipped without structured output is a tool that only humans can use reliably. And the fastest-growing user segment doesn't read terminal output — it parses it.