Before submitting
Area
apps/server
Steps to reproduce
- In Settings → Providers → Claude, set CLAUDE_CONFIG_DIR path to an empty directory, for example
/tmp/claude-empty. An empty config dir produces the same authentication_failed assistant event as an expired session; only the CLI's text differs (Not logged in · Please run /login instead of the refresh failure).
- Open any project and send a message to a Claude thread.
The usage-limit variant: exhaust the 5-hour or weekly window on a Claude subscription, then send a message.
Expected behavior
The turn fails and the error names the real cause and the next step: the login expired, run claude auth login and resend, or the usage limit was reached and when to retry.
Actual behavior
The CLI's own text appears as a plain assistant line ("Failed to authenticate: OAuth session expired and could not be refreshed" or "Not logged in · Please run /login"). The turn then fails with "Claude gave up after repeated API errors." That sentence is T3's own wording: the CLI only reports terminal_reason: "api_error", and resultUserFacingError in apps/server/src/provider/Layers/ClaudeAdapter.ts maps it to the generic message. The structured authentication_failed assistant event that the CLI sends first is never used.
The usage-limit case ends the same way. A rejected rate_limit_event already produces a warning row (#7165), but the CLI treats the underlying 429 as non-retryable and ends the turn as api_error. The CLI does not stamp blocking_limit on this path, even though T3 maps that reason to a usage-limit message, so the quota case still lands on api_error.
Both make a login or quota problem look like a provider outage, so users retry or file provider bug reports.
Impact
Major degradation or frequent failure
Version or commit
main @ 4f782be
Environment
macOS 15, Claude Code CLI 2.1.263, Claude subscription login (OAuth), web client via vp run dev
Logs or stack traces
# SDK message sequence observed by the adapter (auth case)
assistant { error: "authentication_failed", message: { content: [{ type: "text", text: "Not logged in · Please run /login" }] } }
result { subtype: "success", is_error: false, terminal_reason: "api_error" }
# T3 emits: runtime.error "Claude gave up after repeated API errors."
# usage-limit case
system { subtype: "rate_limit_event", rate_limit_info: { status: "rejected", rateLimitType: "five_hour", resetsAt: ... } }
result { subtype: "success", terminal_reason: "api_error" }
Screenshots, recordings, or supporting files

Related: #8869 (open PR for the auth half), #7165 / #6513 (usage-limit warning row), #7878, #7690, #9607 / #9628 (stale process after re-login).
Workaround
Read the plain CLI line above the error. For the login case run claude auth login (with the same CLAUDE_CONFIG_DIR if the instance uses a custom one) and start a new thread; the existing thread's CLI process keeps the stale credentials until it is reaped (#9607).
Before submitting
Area
apps/server
Steps to reproduce
/tmp/claude-empty. An empty config dir produces the sameauthentication_failedassistant event as an expired session; only the CLI's text differs (Not logged in · Please run /logininstead of the refresh failure).The usage-limit variant: exhaust the 5-hour or weekly window on a Claude subscription, then send a message.
Expected behavior
The turn fails and the error names the real cause and the next step: the login expired, run
claude auth loginand resend, or the usage limit was reached and when to retry.Actual behavior
The CLI's own text appears as a plain assistant line ("Failed to authenticate: OAuth session expired and could not be refreshed" or "Not logged in · Please run /login"). The turn then fails with "Claude gave up after repeated API errors." That sentence is T3's own wording: the CLI only reports
terminal_reason: "api_error", andresultUserFacingErrorinapps/server/src/provider/Layers/ClaudeAdapter.tsmaps it to the generic message. The structuredauthentication_failedassistant event that the CLI sends first is never used.The usage-limit case ends the same way. A rejected
rate_limit_eventalready produces a warning row (#7165), but the CLI treats the underlying 429 as non-retryable and ends the turn asapi_error. The CLI does not stampblocking_limiton this path, even though T3 maps that reason to a usage-limit message, so the quota case still lands onapi_error.Both make a login or quota problem look like a provider outage, so users retry or file provider bug reports.
Impact
Major degradation or frequent failure
Version or commit
main @ 4f782be
Environment
macOS 15, Claude Code CLI 2.1.263, Claude subscription login (OAuth), web client via
vp run devLogs or stack traces
Screenshots, recordings, or supporting files
Related: #8869 (open PR for the auth half), #7165 / #6513 (usage-limit warning row), #7878, #7690, #9607 / #9628 (stale process after re-login).
Workaround
Read the plain CLI line above the error. For the login case run
claude auth login(with the sameCLAUDE_CONFIG_DIRif the instance uses a custom one) and start a new thread; the existing thread's CLI process keeps the stale credentials until it is reaped (#9607).