ClaudeWrapper.Auth (ClaudeWrapper v0.8.0)

Copy Markdown View Source

Detect which auth strategy the Claude Code CLI will use, and classify auth-related CLI failures.

This is a standalone, env-only introspection module. It is distinct from ClaudeWrapper.Commands.Auth, which shells out to claude auth (login/logout/status/setup-token). Nothing here spawns a subprocess or reads the filesystem.

Detection

Claude Code resolves auth at invocation time by inspecting a few environment variables, falling back to credentials stored under ~/.claude/ when none are set. detect/0 mirrors that precedence as a cheap, synchronous, env-only check so hosts can introspect the active mode before spawning a turn.

It is not a liveness check -- a reported :subscription strategy only means "no env auth set"; the user might not have run claude login yet. Use ClaudeWrapper.Commands.Auth.status/1 for that.

Precedence (first match wins):

  1. CLAUDE_CODE_USE_BEDROCK truthy -> :bedrock
  2. CLAUDE_CODE_USE_VERTEX truthy -> :vertex
  3. ANTHROPIC_API_KEY non-empty -> :api_key
  4. CLAUDE_CODE_OAUTH_TOKEN non-empty -> :oauth_token
  5. Otherwise -> :subscription

Cloud-provider strategies (Bedrock, Vertex) take precedence because they redirect ALL traffic regardless of API key presence.

Failure classification

classify_failure/3 inspects a failed claude invocation and decides whether it looks auth-shaped. It returns the matching auth_error_kind/0 atom, or nil when the failure is not auth-related. It is conservative on purpose: a false positive turns a legitimate non-auth failure into an "auth error" surprise, so the classifier prefers to miss an auth error rather than misclassify a non-auth one.

Two recent fixes are preserved from the Rust reference:

  • A model-not-found / model-access failure (a bad --model id) must not be classified as auth, even when it carries a 403/404 HTTP status. It returns nil.
  • A --bare invocation with a missing API key prints "Not logged in" to stdout with empty stderr; that surfaces as :not_authenticated.

Example

summary = ClaudeWrapper.Auth.detect()
summary.strategy
#=> :subscription

ClaudeWrapper.Auth.classify_failure(1, "", "HTTP 401 Unauthorized")
#=> :invalid_credentials

ClaudeWrapper.Auth.classify_failure(1, "no match found", "")
#=> nil

Summary

Types

Best-effort classification of an auth-related CLI failure.

Active auth strategy, as inferred from the host environment.

Functions

Inspect a failed claude invocation and decide whether it looks auth-shaped.

Detect the active auth strategy from the current process environment.

Like detect/0, but reads from a caller-provided env map.

Types

auth_error_kind()

@type auth_error_kind() ::
  :not_authenticated
  | :expired
  | :invalid_credentials
  | :rate_limit
  | :provider_error
  | :other

Best-effort classification of an auth-related CLI failure.

  • :not_authenticated -- no credentials at all. Fix: run claude login or set one of the auth env vars.
  • :expired -- stored credentials existed but are expired. Fix: re-run claude login.
  • :invalid_credentials -- credentials were presented but rejected (wrong/revoked key, stale token).
  • :rate_limit -- authenticated but rejected for rate limit / quota / billing reasons. Different remediation: wait, top up, or switch keys -- not "log in again."
  • :provider_error -- Bedrock or Vertex provider error (cloud creds missing or rejected). The fix lives in the cloud provider's auth.
  • :other -- looked auth-shaped (HTTP 401/403, the word "auth") but did not match a more specific pattern.

strategy()

@type strategy() :: :bedrock | :vertex | :api_key | :oauth_token | :subscription

Active auth strategy, as inferred from the host environment.

  • :bedrock -- CLAUDE_CODE_USE_BEDROCK is truthy. Requests route to AWS Bedrock; AWS credentials are resolved separately by the SDK.
  • :vertex -- CLAUDE_CODE_USE_VERTEX is truthy. Requests route to Google Vertex; GCP credentials are resolved separately.
  • :api_key -- ANTHROPIC_API_KEY is set. Direct API access.
  • :oauth_token -- CLAUDE_CODE_OAUTH_TOKEN is set (typically from claude setup-token).
  • :subscription -- no auth env var set. The CLI looks for stored credentials under ~/.claude/. May or may not actually be authenticated -- this reports "the env doesn't pin anything," not "you are logged in."

See the module docs for precedence rules.

Functions

classify_failure(exit_code, stdout, stderr)

@spec classify_failure(integer(), String.t(), String.t()) :: auth_error_kind() | nil

Inspect a failed claude invocation and decide whether it looks auth-shaped.

Returns the matching auth_error_kind/0 atom only when the patterns are confident enough to risk relabeling, and nil otherwise.

exit_code is accepted for parity with the Rust signature and future use; the current heuristics match only against the lowercased stdout/stderr text. The patterns are intentionally narrow:

  • model-not-found / model-access phrasing -> nil (a bad --model id is a typo, not a credential problem, even with a 403/404 status)
  • "not authenticated" / "not logged in" / "claude login" / "run /login" / "no credentials" / "no auth" -> :not_authenticated
  • "expired" / "session has expired" / "token expired" -> :expired
  • "invalid api key" / "invalid token" / "401" / "unauthorized" / "403" / "forbidden" -> :invalid_credentials
  • "rate limit" / "too many requests" / "429" / "quota" -> :rate_limit
  • "bedrock" or "vertex" alongside an auth signal -> :provider_error
  • a bare "auth" / "credential" mention in stderr -> :other

detect()

@spec detect() :: ClaudeWrapper.Auth.Summary.t()

Detect the active auth strategy from the current process environment.

Cheap; no subprocess, no filesystem reads. Reads the real process env via System.get_env/0.

detect_from(env)

@spec detect_from(%{optional(String.t()) => String.t()}) ::
  ClaudeWrapper.Auth.Summary.t()

Like detect/0, but reads from a caller-provided env map.

Exposed for tests and for hosts that want to introspect a child environment they are about to spawn under. The map is keyed by env-var name, matching the shape of System.get_env/0.