CLI Error Codes
CLISubprocessError is the error a CLISubprocessBridge throws when the CLI it runs fails, and CLI_ERROR names nine codes for it. AgentOS ships two bridges, for the claude and gemini binaries behind the claude-code-cli and gemini-cli providers (CLI Providers). Those two bridges and their providers are the only AgentOS code that raises these errors: the CLI Registry reports a missing binary in its scan result and throws none of them. This page gives the class, each code, and when the built-in bridges and providers raise it.
CLISubprocessError Class
A bridge subclass builds the error in its classifyError() method, and the bridge's execute() and stream() throw what that method returns. The class is tied to no binary: binaryName is a constructor argument, so a bridge for ffmpeg or git uses the same class.
Class Structure
class CLISubprocessError extends Error {
/** Error code: an open string, not a fixed union. Each CLI defines its own. */
readonly code: string;
/** The binary that failed (e.g. 'claude', 'gemini', 'ffmpeg'). */
readonly binaryName: string;
/** Human-readable fix instructions shown to the user. */
readonly guidance: string;
/** Whether the caller can retry or fall back. */
readonly recoverable: boolean;
/** Optional underlying error or extra context. */
readonly details?: unknown;
}
guidance and recoverable hold what the code that builds the error passes. No AgentOS code reads either field (What AgentOS does with these errors).
Constructor
new CLISubprocessError(
message: string, // human-readable error description
code: string, // error code string (use CLI_ERROR constants or your own)
binaryName: string, // the CLI binary that failed
guidance: string, // actionable fix instructions shown to the user
recoverable?: boolean, // true if the caller should attempt retry/fallback (default false)
details?: unknown, // optional underlying error or extra context
)
Example
throw new CLISubprocessError(
'ffmpeg not found.',
CLI_ERROR.BINARY_NOT_FOUND,
'ffmpeg',
'Install ffmpeg: brew install ffmpeg',
false,
);
CLI_ERROR Constants
CLI_ERROR maps nine names to strings of the same spelling. They are suggestions: code accepts any string, and the two built-in bridges pass the same strings as literals.
import { CLI_ERROR } from '@framers/agentos/sandbox/subprocess';
The table lists where the built-in bridges and providers raise each code and the recoverable value they set. The sections after it give the exact conditions.
| Code | Raised by | recoverable |
|---|---|---|
BINARY_NOT_FOUND | Provider initialize(); classifyError() on ENOENT | false |
NOT_AUTHENTICATED | Provider initialize(); classifyError() on stderr text | false |
VERSION_OUTDATED | Nothing | Not set |
SPAWN_FAILED | classifyError() on EACCES | false |
TIMEOUT | classifyError() on a timeout, or on a signal other than the caller's abort | true |
CRASHED | classifyError() for every other failure; provider generateCompletion() and generateCompletionStream() on an error result | true |
RATE_LIMITED | classifyError() on stderr text | true |
PERMISSION_DENIED | Nothing | Not set |
CONTEXT_TOO_LONG | classifyError() on stderr text | false |
classifyError() in both bridges (ClaudeCodeCLIBridge.ts, GeminiCLIBridge.ts) checks its conditions in one order and returns the first that matches: a call cancelled through its abort signal, a timeout or a signal, authentication, rate limit, context length, ENOENT, EACCES, and CRASHED when none does. The text checks are substring matches on the process's stderr, lowercased first, so the case of the CLI's wording does not matter.
BINARY_NOT_FOUND
- The
initialize()ofClaudeCodeProviderandGeminiCLIProviderthrows it when the bridge'scheckBinaryInstalled()reports the binary as not installed. That check runswhich <binary>and then<binary> --version, and a failure of either command reads as not installed. classifyError()returns it when the spawn fails withENOENT.- The providers'
checkHealth()reports the same condition without throwing, asdetails.error: 'BINARY_NOT_FOUND'.
To fix it, install the CLI (npm install -g @anthropic-ai/claude-code or npm install -g @google/gemini-cli, the commands the error's guidance gives) and make sure the process that runs AgentOS has the binary's directory on its PATH.
NOT_AUTHENTICATED
initialize()throws it when the bridge'scheckAuthenticated()returnsfalse. The check pipes a one-line prompt (Reply with exactly: pong) through the CLI with a 30-second timeout, and any failure of that run reads as not authenticated: a timeout, a rate limit or a crash as well as a missing login.classifyError()returns it when stderr containsnot logged in,authenticationorunauthorized, in any case; the Gemini bridge also matchessign in.checkHealth()reports it without throwing, asdetails.error: 'NOT_AUTHENTICATED'.
To fix it, run claude or gemini in a terminal and complete the login. When the login is in place and initialize() still throws this code, run the CLI by hand to see the failure the check reported as a missing login.
VERSION_OUTDATED
No AgentOS code raises it. The constant and both providers' code types declare it for a bridge that checks a version. checkBinaryInstalled() returns the version it parses from the --version output (the first x.y.z it finds, or unknown) and compares it with nothing.
SPAWN_FAILED
classifyError() returns it when the spawn fails with EACCES, a permission error. Check the execute permission on the binary.
TIMEOUT
classifyError() returns it when execa, which runs the process, marks the failed run timedOut or isTerminated:
timedOutmeans the run passed itstimeout, a field of theBridgeOptionsgiven toexecute()orstream()(120000 ms when unset). The two providers pass theirrequestTimeoutconfig value, also 120000 ms by default.isTerminatedis true whenever a signal ended the process (result.jsin execa 10.0.1), so a process killed from outside carries this code. A call cancelled throughabortSignalis ended by a signal too, but execa also marks itisCanceled, and the bridges report it asREQUEST_ABORTED(Codes Outside CLI_ERROR).
To allow more time, pass a larger timeout to a bridge you call yourself, or set requestTimeout in the config the provider is initialized with. generateText() and the other helpers initialize the provider with a key and a base URL only, so their CLI calls run on the default.
CRASHED
classifyError()returns it for every failure the earlier checks do not match, a non-zero exit code among them. Itsguidanceends with the last 500 characters of stderr.- The providers'
generateCompletion()throws it when the CLI exits normally and its JSON result carriesis_error: true. - The providers'
generateCompletionStream()ends with it when the CLI flags itsstream-jsonresult as an error:is_error: truefrom Claude Code,status: 'error'from the Gemini CLI. The message isClaude Code returned an error:orGemini CLI returned an error:followed by the CLI's own message for the failure.
RATE_LIMITED
classifyError() returns it when stderr contains rate limit, too many requests or 429, in any case; the Gemini bridge also matches quota and RESOURCE_EXHAUSTED. Its guidance says to wait a few minutes and try again.
PERMISSION_DENIED
No AgentOS code raises it, and the providers' code types leave it out. The built-in bridges report a spawn refused with EACCES as SPAWN_FAILED.
CONTEXT_TOO_LONG
classifyError() returns it when stderr contains context together with too long or token limit, in any case; the Gemini bridge also accepts exceeds. Start a new conversation or use a model with a larger context window.
Codes Outside CLI_ERROR
The two providers' code types add codes of their own (ClaudeCodeProviderError.ts, GeminiCLIProviderError.ts):
| Code | Provider | Raised when |
|---|---|---|
REQUEST_ABORTED | Both | A call is cancelled through its abortSignal: classifyError() returns it, with recoverable: false, for a run execa marks isCanceled. It is the code the HTTP providers give a caller's abort, and isRetryableError() never counts it as retryable |
EMBEDDINGS_NOT_SUPPORTED | Both | generateEmbeddings() is called: neither provider serves embeddings |
UNKNOWN | Both | A completion is requested while the provider is not initialized |
SCHEMA_PARSE_FAILED | Claude Code | Never. generateCompletion() retries a reply that does not parse as the tool-call shape once without the schema and returns it as text; the streaming path returns such a reply as text |
TOOL_PARSE_FAILED | Gemini CLI | Never. The type declares it and nothing raises it |
Custom Error Codes
The code field is an open string, not a fixed union. A bridge can define codes beyond the common set:
// Custom code for a media CLI
new CLISubprocessError(
'Codec not found',
'CODEC_NOT_FOUND', // custom code
'ffmpeg',
'Install the required codec: apt install libx264-dev',
false,
);
// Custom code for a cloud CLI
new CLISubprocessError(
'Stack deployment failed',
'DEPLOY_FAILED', // custom code
'aws',
'Check CloudFormation events: aws cloudformation describe-stack-events --stack-name ...',
true,
);
What AgentOS Does with These Errors
-
Bridge.
execute()andstream()throw the errorclassifyError()returns. -
Provider, non-streaming.
generateCompletion()lets the bridge's error through. -
Provider, streaming.
generateCompletionStream()reads the bridge's stream to its end, where the bridge awaits the CLI's exit, and yields its final chunk after that. It does not throw when the bridge fails or when the CLI reports an error in its output. Either way the stream ends with a final chunk that carries anerror:- When the bridge throws, the
errorholds the error's message, itscodeand, asdetails, the error object. A non-zero exit after the CLI's result line ends the stream this way, in place of the reply. A call cancelled throughabortSignalends it this way too, withtype: 'abort'beside theREQUEST_ABORTEDcode: the chunk theabortSignalcontract ofIProviderasks a streaming provider for, which the completion gateway andstreamText()read as the caller's own stop. - When the CLI flags its result as an error, the
errorholds theCRASHEDerrorgenerateCompletion()throws for an error result: its message, itscodeand, asdetails, the error object. - When the CLI writes an error event into its
stream-jsonoutput, theerrorholds only a message:Claude Code stream error:orGemini CLI stream error:followed by the event's message. It has nocodeand nodetails.
When the CLI reports the failure in its output, as an error result or an error event, a non-zero exit that follows does not replace that error.
- When the bridge throws, the
-
Initialization through the helpers.
generateText(),streamText()and the helpers built on them create the provider withcreateProviderManager(). When the provider'sinitialize()throws, that function throws aProviderInitializationErrorwhosecauseis theCLISubprocessError. -
Fallback. Those helpers decide whether to try the next provider with
isRetryableError(), which reads the error's name, HTTP status,code, type and message. It counts aProviderInitializationErrorand theTIMEOUTcode as retryable andREQUEST_ABORTEDas not, and it never readsrecoverable.
guidance and recoverable are for the host that catches the error: show guidance to the person who can fix the installation, and treat recoverable as the bridge's own hint when you write a retry of your own.
Error Handling Pattern
A host that calls a bridge itself catches the error where it calls:
import { CLISubprocessError } from '@framers/agentos/sandbox/subprocess';
async function run(prompt: string) {
try {
return await bridge.execute({ prompt });
} catch (error) {
if (error instanceof CLISubprocessError) {
console.error(`[${error.code}] ${error.binaryName}: ${error.message}`);
console.error(`Fix: ${error.guidance}`);
if (error.recoverable) {
// The bridge marked the failure as worth another try: here, on a second bridge.
return await fallbackBridge.execute({ prompt });
}
}
throw error;
}
}
Exports
The class and the constants are exported from the subprocess barrel:
import { CLISubprocessError, CLI_ERROR } from '@framers/agentos/sandbox/subprocess';
Source file: src/safety/sandbox/subprocess/errors.ts