macOS 2.1.285: failed OAuth refresh empties sole Keychain credential with one observed session

Status Open
Reported on v2.1.285
Maintainer reply None cached
Activity 0 comments · opened Oct 1, 2026

Summary

Claude Code 2.1.285 on macOS replaced a populated Claude Code-credentials
Keychain record with an empty OAuth cleanup record after a failed refresh.
Only one Claude process was observed in this refresh window. This appears
distinct from the concurrent-session refresh race discussed in closed #76905.

This is a claude.ai subscription login with hosted MCP connectors; an
inference-only setup token is not an equivalent workaround for this workload.

Observed timeline (EDT, September 30–October 1, 2026)

  • September 30 16:49: a fresh login populated the existing credential store;

a real model request subsequently returned successfully.

  • The access-token expiry was approximately October 1 00:49.
  • October 1 around 12:16–12:17: the first observed Claude session in the refresh

window read the Keychain successfully, then failed authentication with
Failed to authenticate: OAuth session expired and could not be refreshed.

  • At 12:17:01, /usr/bin/security performed SecKeychainItemModifyContent.

Item creation time remained September 30; modification time became
20261001161701Z.

  • A bounded subsequent read returned valid JSON, 746 bytes, with

claudeAiOauth.accessToken === "", refreshToken === "", and expiresAt === 0.
The credential fallback file was absent at that point.

  • Subsequent sessions failed immediately. There were no observed concurrent

refreshers, no credential-read confirmation dialogs in this window, and reads
succeeded despite the screen being locked. Screen locking alone therefore
does not explain this occurrence.

The process resolved through the installed Claude symlink to 2.1.285. Some
macOS unified-log entries used an older image label; that label alone is not
our version evidence.

What is verified versus unknown

The installed 2.1.285 JavaScript cleanup path can replace matching stored OAuth
with empty access/refresh strings and expiry zero. A local extracted-code test,
using dummy in-memory stores only, reproduces that replacement and shows the
generation check prevents it when the stored refresh token differs.

The same extracted storage path distinguishes transient write timeouts from
ordinary write failures: ordinary failure can clear the file fallback and
delete the primary, whereas its transient classification preserves both.
That makes a naive external "return an error instead of writing" workaround
unsafe. No credentials or private logs are included in this report.

We do not have the token endpoint's HTTP response or response body for the
incident. The cleanup code is associated with invalid-grant handling, but we
cannot establish whether the server genuinely revoked the refresh token,
whether the classification was correct, or how many internal HTTP attempts
occurred. The observed failure was one session, not evidence of a second
process racing it. Correlation with a long idle period does not prove causation.

Expected behavior / diagnostic request

Please distinguish transient refresh failures from positively confirmed
revocation and avoid destroying the only saved refresh credential on an
ambiguous failure. Preserve explicit logout/account-switch semantics. Provide
redacted refresh diagnostics (HTTP status, classified error and retry count,
never token values) so an incident can be attributed without recording secrets.

If an empty cleanup record is intentional after confirmed revocation, please
document the condition and make the error explain that reauthentication is
required instead of repeatedly attempting an already empty record.

Related: https://github.com/anthropics/claude-code/issues/76905

View original on GitHub ↗