Skip to content

Claude Code OAuth expiry: classify authorization failures and recover sign-in inside BB #3337

Open

Description

@t1mdurden

Consolidated recovery scope

This issue now also tracks #838's request to recover from expired Claude credentials inside BB. It covers both authorization-error classification and recovery; closing #838 does not mean automatic login or recovery has shipped.

  • Preserve the provider's authentication-failure category and render an actionable authorization error.
  • Forward the recovery hint to the product UI and expose an equivalent SDK/CLI recovery path.
  • Attempt normal provider-supported credential refresh automatically where it can complete without user input. If refresh fails or interactive login is required, present a user-triggered sign-in action on the execution host and show its outcome. Do not repeatedly prompt or retry failed authentication indefinitely.
  • After successful authentication, make the original thread usable without restarting the BB server; retain the existing provider-session restart behavior.
  • Verify ordinary expiry, revoked refresh credentials, cancelled/failed login, and successful continuation. A titled auth error alone does not satisfy recovery.

The original #838 screenshot already showed a titled authorization failure, so it is evidence for missing recovery rather than for this report's separate untitled-error defect:

Image

Summary

When the Claude Code CLI's OAuth session expires mid-thread, bb shows the CLI's apology text as an ordinary error row with no title and no action, and there is nothing in the app that lets you sign in again. The one machine-readable "this account is signed out" signal bb computes is written to a debug log and discarded. The user is left in a thread that keeps failing the same way with no route out of the app.

Expected: bb recognises the failure as an authorization failure and offers a way to re-authenticate from the thread.

Versions and environment

  • bb 0.42.1, desktop app
  • macOS 26.x (Darwin 25.3.0), Node 22.23.1 when running from source
  • Claude Code CLI 2.1.252
  • Local project checkout environment, primary machine
  • Verified against main at 4ae0a7c893e9922721e0e367c72aa3a1a2990e7c

Steps to reproduce

  1. Let the Claude Code CLI OAuth session expire (or revoke it) so claude cannot refresh it.
  2. Open an existing Claude Code thread in bb and send a turn.
  3. Observe the reply, then look for any way to re-authenticate inside bb.

Smallest faithful automated reproduction, at the exact code path — feed the Claude bridge the assistant error message the CLI actually emits, followed by its result:

// plugins/provider-claude-code/src/delta-translation.test.ts
harness.translate(sdkMessage({
  type: "assistant",
  error: "authentication_failed",
  isApiErrorMessage: true,
  message: { content: [{ type: "text", text: "Failed to authenticate: OAuth session expired and could not be refreshed" }], model: "<synthetic>", stop_reason: "stop_sequence", stop_sequence: "" },
}));
const events = harness.translate(sdkMessage({
  type: "result", subtype: "error_during_execution", is_error: true,
  errors: ["Failed to authenticate: OAuth session expired and could not be refreshed"],
  usage: {}, modelUsage: {},
}));
// actual:   errorInfo.category === "unknown",     providerCode === "error_during_execution"
// expected: errorInfo.category === "unauthorized", providerCode === "authentication_failed"

The exact wire shape above is taken from a real Claude Code transcript ("error": "authentication_failed", "isApiErrorMessage": true, CLI 2.1.252) and matches the fixture the repo's own bridge test already uses at bridge.test.ts#L464-L490.

Expected vs actual

Actual, in the timeline:

Failed to authenticate: OAuth session expired and could not be refreshed

as an untitled error row. No banner, no button, no hint that the account is the problem. The composer stays enabled, so every further message repeats the same failure. The only remedy is to leave bb, run claude /login in a terminal, and come back.

Expected: the row is classified as an authorization failure, and the thread offers a way to sign in again.

Evidence

Two independent places drop the auth signal.

1. The classification is lost between the assistant message and the turn result. The CLI reports the reason on the assistant message (error: "authentication_failed"); the failure category is computed from the result message alone:

2. The authRequired recovery hint is swallowed in the host daemon. The bridge classifies this case correctly and notifies (provider/recovery, kind: "authRequired") at bridge.ts#L1642-L1650. It reaches the runtime intact, and the only production consumer is runtime-manager.ts#L1248-L1259, whose whole body is one logger?.debug(...) call. The adjacent onProcessExit handler forwards through this.options.onEvent; onProviderRecovery never does. Nothing in apps/server or apps/app ever learns the account is signed out.

3. The remedy bb already knows about is never rendered. provider-maintenance.ts#L337 sets loginCommand: "claude /login" on every ProviderHealthResult, and it reaches the client on SystemProviderState. No component in apps/app/src reads it. The only surface that renders unauthenticated / expired at all is UsageLimitsSettingsSection.tsx, as inert prose ("Sign in again, then reload usage.").

What you ruled out

  • Not Claude threads reuse stale CLI process after terminal authentication failure #2097. That one was about a thread reusing a stale CLI process after the user re-authenticated in a terminal; its fix (arming restartBeforeNextTurn) is in and works. This report is about the user never being told what happened and having no way to re-authenticate from inside bb.
  • Not a wedged thread. The composer stays enabled — EmbeddedThreadChat gates on status === "stopping" and provisioning states only. The user can keep typing; every turn just fails the same way.
  • Not an infinite retry loop. The session restart is armed lazily and consumed only on the next user-initiated turn (bridge.ts#L1508-L1509), so the repeat is user-driven.
  • Not fixed by health polling alone. getClaudeProviderHealth reports expired from credentials.expiresAt. A refresh token revoked while expiresAt is still in the future leaves health at ready while every turn fails — which is exactly the "could not be refreshed" wording in the error.

Suggested priority and effort

Medium. It hits every user whose Claude subscription session lapses, which is routine; the workaround (leave the app, run claude /login) is discoverable only if you already know it. Effort is small for the classification fix plus a thread-level sign-in affordance; forwarding the swallowed authRequired hint is a separate, larger change because it is a host-daemon→server wire addition.

AGENT GENERATED

Activity

  1. added theissue type on Sep 9, 2026
  2. added
    providersCross-provider bridges, models, login
    partial-reproBug partially reproduced; some claims unverified; see linked report
    on Sep 9, 2026
  3. bb-slop-cop commented on Sep 9, 2026

    @bb-slop-cop
    Contributor

    🚨 SLOP COP 🚨 · new-issue-autopilot

    Bug · Medium priority · High effort — applied providers, provider-claude-code, and partial-repro; no existing classification values were replaced.

    Reproduction report: PARTIALLY REPRODUCED, with high confidence in the classification root cause. The repository-derived regression failed in both clean checkouts: assistant authentication codes are lost before result classification, producing unknown instead of unauthorized. Source inspection also confirms the recovery hint is only logged. Live OAuth expiry and desktop recovery were not exercised.

    No new PR: open PR #3338 already links this issue.

  4. changed the title [-]Claude Code OAuth expiry reads as an untitled error and bb offers no way to sign in again[/-] [+]Claude Code OAuth expiry: classify authorization failures and recover sign-in inside BB[/+] on Sep 9, 2026
  5. SawyerHood commented on Sep 18, 2026

    @SawyerHood
    Collaborator

    Consolidating #3894 into this existing sign-in recovery tracker at the maintainer's request.

    Additional user case: on macOS desktop bb 0.43.1 with Claude Code 2.1.276, a chat fails with "Failed to authenticate: OAuth session expired and could not be refreshed". BB says to run claude on the machine. Starting Claude in a terminal, refreshing provider usage, and retrying the chat do not restore it. Preserve the requirement for explicit login guidance on the correct execution host and verification that the original thread can continue after successful authentication.

    Evidence boundary: synthetic tests in two clean checkouts confirm that usage refresh rereads expired credentials without renewing them and that replacing the selected credential restores usage. The actual chat failure after terminal startup was not independently reproduced; terminal startup alone does not establish successful reauthentication. A stale Keychain entry masking a fresh file is a hypothesis, not a diagnosed cause. Report: https://get-bb.github.io/reports/issues/3894.html

    Investigate credential-directory selection (#3607) and live-session credential reuse (#1654) if the reporter confirms a successful explicit login and chats still fail. This consolidation does not assert either is the cause or that recovery has shipped. Keep the new reproduction context here rather than starting a duplicate recovery implementation.

    AGENT GENERATED

  6. markspen commented on Sep 21, 2026

    @markspen

    Live macOS evidence for the automatic-refresh portion of this recovery scope is now documented in #3607: #3607 (comment)

    In short, Claude Code 2.1.278 successfully runs a direct request using a valid refresh token while BB 0.43.3 reports the provider as expired. BB stops on the Keychain access-token expiresAt before attempting usage. claude auth login while already logged in did not rotate that Keychain record, so the current guidance and refresh button cannot recover the meter. This is distinct from a truly revoked/failed refresh credential and gives a live case for the checklist item “Attempt normal provider-supported credential refresh automatically where it can complete without user input.”

    Investigation thread: thr_atnbz83rxg.

    AGENT GENERATED

  7. karstenisdope commented on Sep 21, 2026

    @karstenisdope

    Another data point on 0.43.3, from a long-running plugin thread.

    A Claude Code seat died with Failed to authenticate: OAuth session expired and could not be refreshed. Four occurrences in two days: three on 2026-09-19 between 16:26 and 16:30, one on 2026-09-20 at 21:02. No sign-in action appeared in the UI, and the thread stayed unusable until I ran claude auth login in a terminal and reopened bb.

    Reading provider-claude-code/dist/host.js in the app bundle, bb writes the seat a credentials file with claudeAiOauth.refreshToken deleted and handles renewal itself. So the CLI has no way to renew, and the error text comes from Claude Code running without a refresh token. Since every Claude Code client on the machine shares one Keychain login and each renewal rotates the key, another client renewing looks like a plausible trigger for bb's copy going stale, though I haven't proven that part.

    The recovery items in this issue would have covered it: the seat couldn't renew, there was no in-app sign-in, and the thread needed a bb restart after I signed in elsewhere.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    partial-reproBug partially reproduced; some claims unverified; see linked reportprovider-claude-codeBuilt-in plugin: provider-claude-codeprovidersCross-provider bridges, models, login

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions