This document defines the structured error format used when Verda CLI runs in --agent mode. All errors are written to stderr as JSON so that calling agents can parse them programmatically.
Every error follows this JSON envelope:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable explanation",
"details": {}
}
}| Field | Type | Description |
|---|---|---|
code |
string | Machine-readable error category (UPPER_SNAKE_CASE). Agents use this to decide what to do next. |
message |
string | Human-readable explanation. Agents can show this to the user or use it for context. |
details |
object | Optional structured data specific to the error code. Contents vary by code. |
Exit code: 2
Required CLI flags were not provided. The agent should read details.missing and either provide the missing values or ask the user.
{
"error": {
"code": "MISSING_REQUIRED_FLAGS",
"message": "required flags not provided",
"details": {
"missing": ["--instance-type", "--os", "--hostname"]
}
}
}Agent action: Read the missing flags, determine values, and retry the command with all required flags.
Exit code: 2
A destructive action (delete, shutdown) was attempted without --yes. The agent should confirm with the user, then retry with --yes.
{
"error": {
"code": "CONFIRMATION_REQUIRED",
"message": "destructive action \"delete\" requires --yes in agent mode",
"details": {
"action": "delete"
}
}
}Agent action: Ask the user for confirmation. If confirmed, retry with --yes.
Exit code: 2
A command tried to open an interactive prompt (select, text input, etc.) in agent mode. The details include the prompt text and available choices so the agent can determine the correct flag to pass.
{
"error": {
"code": "INTERACTIVE_PROMPT_BLOCKED",
"message": "select prompt not available in agent mode",
"details": {
"prompt_type": "select",
"prompt": "Select instance (type to filter)",
"choices": ["gpu-runner ● running 1V100.6V FIN-01 203.0.113.10", "Cancel"]
}
}
}Agent action: Determine which flag provides the needed input and retry with that flag.
Exit code: 3
Authentication failed. Credentials are missing, invalid, or expired.
{
"error": {
"code": "AUTH_ERROR",
"message": "no credentials configured\n\nRun \"verda auth login\" to set up your credentials",
"details": {
"status": 401
}
}
}Agent action: Tell the user to run verda auth login.
Exit code: 4
The Verda Cloud API returned an error not covered by a more specific code.
{
"error": {
"code": "API_ERROR",
"message": "API error 500: internal server error",
"details": {
"status": 500
}
}
}Agent action: Show the error to the user. May be transient (retry) or permanent.
Exit code: 5
The requested resource does not exist.
{
"error": {
"code": "NOT_FOUND",
"message": "not found",
"details": {
"status": 404
}
}
}Agent action: Verify the resource ID is correct. List resources to find the right one.
Exit code: 2
POST /instances requires at least one SSH key and the request carried none. The
API's own text lists an absent value as acceptable and then rejects it, so the
server wording is preserved under details.api_message rather than shown as the
message.
{
"error": {
"code": "SSH_KEY_REQUIRED",
"message": "the API requires at least one SSH key to create an instance, and this request had none: pass --ssh-key <id> (CLI) or ssh_key_ids (MCP); list ids with \"verda ssh-key list\"",
"details": {
"status": 400,
"api_message": "SSH keys can be an array of UUID's, a single UUID string, null value or not defined"
}
}
}Agent action: Call list_ssh_keys (or verda ssh-key list), then retry with
at least one key id. Creating a key first is add_ssh_key / verda ssh-key add.
Exit code: 6
The account does not have enough balance for the requested operation.
{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "insufficient balance",
"details": {
"status": 402
}
}
}Agent action: Show balance to user (verda cost balance), suggest adding funds.
Exit code: 2
A flag value failed validation.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "invalid value for hostname: too long",
"details": {
"field": "hostname",
"reason": "too long"
}
}
}Agent action: Fix the invalid value and retry.
Exit code: 1
Catch-all for errors that don't match a more specific code.
{
"error": {
"code": "ERROR",
"message": "unexpected: connection refused",
"details": null
}
}Agent action: Show the error message to the user.
| Code | Meaning | Agent Should |
|---|---|---|
| 0 | Success | Process stdout JSON |
| 1 | General error | Show error to user |
| 2 | Bad input (missing flags, validation, needs confirmation) | Fix input and retry |
| 3 | Authentication error | Tell user to run verda auth login |
| 4 | API error (server-side) | Show error, possibly retry |
| 5 | Resource not found | Verify resource ID |
| 6 | Insufficient balance | Show balance, suggest adding funds |
Errors are classified in this priority order:
- Already an AgentError (from explicit checks in commands) -- returned as-is
- CLI usage errors (
cmdutil.UsageErrorfrom flag/argument misuse) -- VALIDATION_ERROR, exit 2 - SDK
APIError-- mapped by HTTP status code (401/403 -> AUTH_ERROR, 404 -> NOT_FOUND, 402 -> INSUFFICIENT_BALANCE, others -> API_ERROR) - SDK
ValidationError-- mapped to VALIDATION_ERROR with field and reason - Auth-related message heuristic -- messages containing "no credentials configured", "unauthorized", "token expired" -> AUTH_ERROR
- Fallback -- generic ERROR with the original message
- Add the constant to
agent_error.go(exit code + constructor) - Add the code to this document with example JSON and agent action
- Add classification logic in
ClassifyErrorif needed - Add a test in
agent_error_test.go
- Return AgentError directly when the command has specific context (e.g., which flags are missing, which action needs confirmation). This gives agents the richest information.
- Rely on ClassifyError for errors bubbling up from the SDK or other libraries. The classifier handles the common cases automatically.
- Error types:
internal/verda-cli/cmd/util/agent_error.go - Classification:
ClassifyError()in the same file - Entry point:
cmd/verda/main.gocallsClassifyError()on all errors
Tools exposed by verda mcp serve reuse this contract with one transport difference: MCP has no stderr/exit codes, so tool failures arrive as tool results with isError: true whose text payload is the same JSON envelope. Argument-contract errors produced inside the MCP server use these codes:
CONFIRMATION_REQUIRED— billing/destructive tool called withoutconfirm: true;details.actionnames the gated actionMISSING_REQUIRED_FLAGS— required tool argument absent;details.missinglists themVALIDATION_ERROR— argument type/out-of-set value rejected;details.field+details.reason
{"error": {"code": "CONFIRMATION_REQUIRED", "message": "action \"delete\" creates billing or destructive changes and requires an explicit confirm: true argument", "details": {"action": "delete"}}}One error type, one classifier. MCP argument errors are plain cmdutil.AgentError values — not a parallel type — and toolErrorResult renders every failure through ClassifyError, the same funnel the CLI uses. So an API 404 reaches an agent as NOT_FOUND over MCP exactly as it does on the CLI, and a new code added to the classifier appears on both surfaces without touching either renderer.
Changed 2026-08-12: MCP previously carried a private
argErrortype, so only argument errors had a code and every other failure degraded to a bare string. Tool errors that used to be plain text now arrive as the envelope. Handlers still returningmcp.NewToolResultError(err.Error())directly bypass this — converting the remaining call sites is tracked separately.
See internal/verda-cli/cmd/mcp/README.md for the full tool reference.