Files
claude-agent-acp/src/startup-failure.ts
Ben Brandt 38cf656fc9 feat: report why Claude Code could not start a session (#1508)
When Claude Code refuses to start (invalid proxy URL, wrong organization
sign-in, a required sandbox that can't run, …), `session/new`,
`session/load`, `session/resume` and `session/fork` used to fail with a
generic `Claude Code process exited with code 1. stderr: …` error.

This sets `CLAUDE_CODE_STARTUP_FAILURE_RESULTS=1` on the query env, so
the CLI writes an error result with a `startup_failure_reason` before
exiting on a known startup failure. The SDK then rejects
`initializationResult()` with an error that carries
`startupFailureReason`. The new `src/startup-failure.ts` maps it to an
ACP error:

| `startupFailureReason` | ACP error |
|---|---|
| `gateway_signin_required`, `org_pin_mismatch`, `org_config_refused` |
`authRequired` (signing in again, or with another account, fixes it) |
| `cwd_unavailable` | `invalidParams` (matches the existing cwd
validation) |
| any other reason | `internalError` |

The message is the CLI's own text, which says what to fix (e.g. `Invalid
proxy URL in HTTPS_PROXY: … Fix or unset HTTPS_PROXY and restart Claude
Code.`), and `data.startupFailureReason` carries the reason. Errors
without a reason (older CLIs, unknown causes) pass through unchanged,
and the existing `sessionNotFound` mapping for resumes is untouched.

Checked against the real CLI (SDK 0.3.316): with `HTTPS_PROXY="not a
url"`, `initializationResult()` rejects with `startupFailureReason:
"proxy_invalid"` when the variable is set, and with a plain exit-code
error without it.

## Testing

- v1: `create-session-options.test.ts` covers the env var, each mapping,
and pass-through of errors without a reason.
- v2: `acp-v2.test.ts` covers the `session/new` error over the wire.
- `npm run build`, `npm run check`, `npm run test:run` pass. The
subprocess integration tests also passed locally with this change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 11:23:33 +00:00

43 lines
2.0 KiB
TypeScript

import { RequestError } from "@agentclientprotocol/sdk/unstable";
import type { SDKStartupFailureReason } from "@anthropic-ai/claude-agent-sdk";
/** Makes the CLI write an `error_during_execution` result with a
* `startup_failure_reason` before it exits on a known startup failure. Without
* it, some failures (e.g. `proxy_invalid`) end with stderr and an exit code
* alone. The SDK then rejects `initializationResult()` with an error that
* carries the reason as `startupFailureReason`. */
export const STARTUP_FAILURE_RESULTS_ENV = { CLAUDE_CODE_STARTUP_FAILURE_RESULTS: "1" };
/** The SDK's prefix on the message of an error built from an error result. */
const ERROR_RESULT_PREFIX = "Claude Code returned an error result: ";
/** Reasons that signing in again, or with another account, can fix. */
const AUTH_REQUIRED_REASONS: ReadonlySet<SDKStartupFailureReason> = new Set([
"gateway_signin_required",
"org_pin_mismatch",
"org_config_refused",
]);
/** The ACP error for a CLI that refused to start, or `undefined` when `error`
* carries no `startupFailureReason` (older CLIs, failures without a known
* cause). The message is the CLI's own, which says what to fix, e.g.
* `Invalid proxy URL in HTTPS_PROXY: … Fix or unset HTTPS_PROXY and restart
* Claude Code.`; `data.startupFailureReason` carries the reason. */
export function startupFailureError(error: unknown): RequestError | undefined {
if (!(error instanceof Error)) return undefined;
const reason = (error as { startupFailureReason?: unknown }).startupFailureReason;
if (typeof reason !== "string" || reason === "") return undefined;
const message = error.message.startsWith(ERROR_RESULT_PREFIX)
? error.message.slice(ERROR_RESULT_PREFIX.length)
: error.message;
const data = { startupFailureReason: reason };
if (AUTH_REQUIRED_REASONS.has(reason as SDKStartupFailureReason)) {
return RequestError.authRequired(data, message);
}
if (reason === "cwd_unavailable") {
return RequestError.invalidParams(data, message);
}
return RequestError.internalError(data, message);
}