P4: Fail Fast with Actionable Errors
Definition
CLI tools MUST detect invalid state early, exit with a structured error, and tell the caller three things: what failed, why, and what to do next. An error that says "operation failed" gives an agent nothing to act on.
Why Agents Need It
Agents operate in a retry loop: attempt, observe, decide. When an error is vague or unstructured — a bare stack trace, a one-word failure, a mixed-channel splurge — the agent cannot tell whether to retry, re-authenticate, fix configuration, or escalate to the user. Distinct exit codes with actionable messages let the agent act correctly on the first read. The difference between exit code 77 (re-authenticate) and exit code 78 (fix config) determines whether the agent retries OAuth or asks the user to check their config file. Getting that wrong wastes entire conversation turns.
Requirements
MUST:
-
Parse arguments with
try_parse()instead ofparse(). Clap'sparse()callsprocess::exit()directly, bypassing custom error handlers — which means--output jsoncannot emit JSON parse errors.try_parse()returns aResultthe tool can format:let cli = Cli::try_parse()?; -
Error types map to distinct exit codes. At minimum:
Code Meaning 0 Success 1 General command error 2 Usage / argument error 77 Auth / permission error 78 Configuration error -
Every error message contains what failed, why, and what to do next. Example:
Authentication failed: token expired (expires_at: 2026-03-25T00:00:00Z). Run `tool auth refresh` or set TOOL_TOKEN.
SHOULD:
- Error types use a structured enum (via
thiserrorin Rust) with variant-to-kind mapping for JSON serialization. Agents match on error kinds programmatically rather than parsing message text. - Config and auth validation happen before any network call. A three-tier dependency gating pattern (meta-commands, local-only commands, network commands) fails at the earliest possible point.
- Error output respects
--output json: JSON-formatted errors go to stderr when JSON output is selected.
Evidence
Cli::try_parse()inmain(), notCli::parse().- Error enum with
#[derive(Error)]and distinct variants for config, auth, and command errors. exit_code()method on the error type returning variant-specific codes.kind()method returning a machine-readable string for JSON serialization.run()function returningResult<(), AppError>, not callingprocess::exit()internally.- Error messages containing remediation steps ("run X" or "set Y") alongside the cause.
Anti-Patterns
Cli::parse()anywhere in the codebase — it silently prevents JSON error output.process::exit()in library code or command handlers. Onlymain()may call it, after all error handling.- A single catch-all error variant that maps everything to exit code 1.
- Error messages that state the symptom without the cause or fix ("Error: request failed").
- Panics (
unwrap(),expect()) on recoverable errors in production code paths.
Measured by check IDs p4-bad-args, p4-process-exit, p4-unwrap, p4-exit-codes. Run
agentnative check --principle 4 . against your CLI to see each.