Skip to content

fix: update credential handling to prioritize primary service for token refresh#99

Open
errhythm wants to merge 5 commits intogriffinmartin:mainfrom
errhythm:fix/refresh-stale-credentials
Open

fix: update credential handling to prioritize primary service for token refresh#99
errhythm wants to merge 5 commits intogriffinmartin:mainfrom
errhythm:fix/refresh-stale-credentials

Conversation

@errhythm
Copy link
Copy Markdown
Contributor

Two bugs were found by analyzing debug logs from users hitting the "Claude Code credentials are unavailable or expired" error.

Bug 1 — macOS / keychain (hard failure)
Users with multiple Claude Code accounts have a suffixed active account (e.g. Claude Code-credentials-b28bbb7c). When the token expires, the Claude CLI is invoked to refresh it and succeeds, but the CLI writes the new token back to the primary Keychain entry (Claude Code-credentials), not the suffixed one. refreshAccount(target.source) re-reads the suffixed entry, which still holds the old expiresAt, the post-refresh validity check fails, and refresh_exhausted → credentials_unavailable is emitted. The user sees:

Claude Code credentials are unavailable or expired. Run claude to refresh them.

Fix: if the suffixed account re-read is still stale after a CLI refresh, fall back to reading the primary entry. The fallback is gated on target.source.startsWith(PRIMARY_SERVICE + "-") so it only fires for suffixed accounts.

Bug 2 — all platforms (infinite refresh loop) (Potentially fixes #89)

After refreshIfNeeded returns fresh credentials, getCachedCredentials stores them in accountCacheMap with a 30-second TTL. When the cache expires, getActiveAccount() returns the same ClaudeAccount object from allAccounts — whose credentials.expiresAt was never updated. So refreshIfNeeded sees the original stale expiry, runs another full CLI refresh, and the cycle repeats every 30 seconds indefinitely. Visible in logs as refresh_needed firing with the same expiresAt value on every cache miss, hours after the token was successfully refreshed.

Fix: after a successful refresh, target.credentials is updated in-place on the ClaudeAccount object so subsequent cache misses see the correct expiresAt.

Also now opencode auth login will show the user account details instead of just Claude Pro/Team/Max

◆ Select which Claude Code account to use:
│ ● Claude Pro: john.doe@gmail.com (Claude Code-credentials)
│ ○ Claude Team: john@acme.com

@yvyw yvyw mentioned this pull request Mar 29, 2026
@errhythm
Copy link
Copy Markdown
Contributor Author

errhythm commented Apr 1, 2026

@griffinmartin I see a lot of great changes made and I believe we can use some of the features we have here. Do you want me to update this?

@griffinmartin
Copy link
Copy Markdown
Owner

Hey @errhythm, yeah I'd love for you to update this! There's some really useful stuff here, especially the Bug 1 fix for suffixed keychain accounts and the email display in the account selector.

A few things to keep in mind for the rebase:

  1. Bug 2 is already fixed on mainfix: eliminate idle token consumption via direct OAuth refresh #104 added a direct OAuth refresh path (refreshViaOAuth) and the in-place target.credentials update. So you can drop that part entirely. The main thing to be careful about is not overwriting the current refreshIfNeeded — your Bug 1 primary service fallback should slot in after the existing OAuth + CLI refresh flow, not replace it.

  2. The CLAUDE_CONFIG_DIR tests (keychain.test.ts) don't actually exercise the production code — they just write a file to a temp dir and read it back with readFileSync. They should call the real readCredentialsFile or readAllClaudeAccounts with the env var set so they're testing actual behavior.

  3. discoverConfigDirsForKeychain scans all of $HOME — could you scope it down to dotfiles (dirs starting with .) since Claude config dirs follow that convention? Doing existsSync on every entry in someone's home dir could be slow.

  4. The hint change — you changed the account selector hint from showing the full keychain source name to just "active"/undefined. That's fine if every account has an email to distinguish it, but if readEmailFromConfigDir returns null for some accounts, users lose the ability to tell them apart. Maybe keep the source as a fallback when there's no email?

The Bug 1 fix, configDir passthrough, keychainSuffixForDir, and the regex tightening are all good to go as-is. Looking forward to the update!

errhythm added 2 commits April 4, 2026 19:51
…en refresh

This commit introduces a bug fix for the credential refresh logic, ensuring that if the active account is a suffixed entry and its credentials are stale, the system will fall back to the primary service ("Claude Code-credentials") to retrieve updated tokens. Additionally, the PRIMARY_SERVICE constant is now consistently exported across relevant files to maintain clarity and prevent duplication.
This commit updates the `refreshViaCli` function to accept an optional `configDir` parameter, allowing the CLI to read from and write back to the correct account directory. It also improves logging for credential refresh attempts and modifies the `buildAccountLabels` function to append email addresses when available. Additionally, new tests are added to ensure proper handling of the `CLAUDE_CONFIG_DIR` environment variable across different platforms.
@errhythm errhythm force-pushed the fix/refresh-stale-credentials branch from 42d47bf to 9cba1cf Compare April 4, 2026 14:02
errhythm added 2 commits April 4, 2026 20:30
… improvements

This commit introduces a mock for the child process in the credential tests, allowing for better isolation during testing. Additionally, it improves the formatting of temporary directory creation and JSON file writing in the tests for better readability and consistency. The changes ensure that the tests remain robust while simulating the necessary environment for credential handling.
@errhythm errhythm force-pushed the fix/refresh-stale-credentials branch from 446b7e3 to a72a12a Compare April 4, 2026 15:22
This commit modifies the keychain test file to replace instances of `process.platform` with a hardcoded value of `"darwin"`. This change ensures that the tests can run consistently in a controlled environment, improving test reliability and isolation.
@errhythm
Copy link
Copy Markdown
Contributor Author

errhythm commented Apr 4, 2026

@griffinmartin Please review if it is correct.

ownuun added a commit to ownuun/opencode-claude-auth that referenced this pull request Apr 10, 2026
…ting

Update all config examples in README.md and installation.md to use
`opencode-claude-auth@latest` so OpenCode always fetches the newest
version on startup.

Add a troubleshooting entry and "Updating the plugin" section for the
"You're out of extra usage" / "Third-party apps" 400 error that
affected users on stale cached versions. The fix: ensure @latest in
config, clear ~/.cache/opencode/packages/opencode-claude-auth*, restart
OpenCode, and re-authenticate if needed.

Closes griffinmartin#145, closes griffinmartin#99

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-refresh not working

2 participants