charmbracelet/crush · error
provider %s does not have an OAuth token
Error message
provider %s does not have an OAuth token
What it means
refreshOAuthTokenLocked found the provider but its config has no OAuth token (providerConfig.OAuthToken == nil). Refreshing only applies to OAuth-based providers; API-key providers cannot be refreshed, so the store returns this error instead of attempting an exchange.
Source
Thrown at internal/config/store.go:676
func (s *ConfigStore) RefreshOAuthToken(ctx context.Context, scope Scope, providerID string) error {
key := fmt.Sprintf("%d\x00%s", scope, providerID)
_, err, _ := s.refreshSF.Do(key, func() (any, error) {
return nil, s.refreshOAuthTokenLocked(ctx, scope, providerID)
})
return err
}
// refreshOAuthTokenLocked performs the cross-process single-flighted
// refresh. It is invoked through refreshSF, so at most one goroutine per
// provider runs it at a time within this process.
func (s *ConfigStore) refreshOAuthTokenLocked(ctx context.Context, scope Scope, providerID string) error {
cfg := s.Config()
providerConfig, exists := cfg.Providers.Get(providerID)
if !exists {
return fmt.Errorf("provider %s not found", providerID)
}
if providerConfig.OAuthToken == nil {
return fmt.Errorf("provider %s does not have an OAuth token", providerID)
}
entryToken := providerConfig.OAuthToken
// Acquire the per-provider cross-process refresh lock. This is a
// dedicated lock file, not the config-write lock, and it does not take
// s.mu — so the network exchange below cannot stall unrelated config
// operations. The deadline exceeds the exchange timeout so that a peer
// mid-exchange has time to publish a token we can adopt. Lock ordering:
// the refresh lock is always taken before the config-write lock (via
// SetConfigFields), never the reverse, so no deadlock is possible.
lockCtx, cancel := context.WithTimeout(ctx, refreshLockDeadline)
defer cancel()
release, lockErr := lock.File(lockCtx, s.refreshLockPath(providerID))
if lockErr != nil {
// Could not acquire the lock (peer wedged or deadline hit). Prefer a
// usable token already on disk over forcing our own exchange, which
// would risk reusing a rotated refresh token.
if diskToken := s.usableDiskToken(scope, providerID, entryToken); diskToken != nil {View on GitHub (pinned to 7944b8e522)
Solutions
- Re-authenticate the provider with OAuth (crush login <provider>) so a token exists
- If you intend to use an API key instead, remove the OAuth refresh flow for that provider
- Check the token was written to the same scope (project vs global) the runtime reads
- Restart to drop stale refresh timers pointing at de-OAuthed providers
Example fix
// before
// provider authed with API key only, but refresh still requested:
err := refresh(ctx, "hyper")
// after
pc, _ := store.Config().Providers.Get("hyper")
if pc.OAuthToken == nil {
return nil // API-key provider: nothing to refresh; skip
}
err := refresh(ctx, "hyper") Defensive patterns
Strategy: type-guard
Validate before calling
if pc, ok := store.Config().Providers.Get(providerID); !ok || pc.OAuthToken == nil {
return fmt.Errorf("provider %q has no OAuth token; use api-key auth or re-run 'crush login'", providerID)
} Type guard
func hasOAuthToken(store *config.ConfigStore, id string) bool {
pc, ok := store.Config().Providers.Get(id)
return ok && pc.OAuthToken != nil
} Try / catch
err := doAuthenticatedRequest(ctx, providerID)
if err != nil && strings.Contains(err.Error(), "does not have an OAuth token") {
// non-OAuth provider: fall back to api-key auth path
return doAPIKeyRequest(ctx, providerID)
} Prevention
- Check OAuthToken presence before wiring up refresh single-flight for a provider
- Drop refresh timers when a provider switches from OAuth to API-key auth
- Save tokens and API keys to the same scope the runtime reads
- Re-run 'crush login' if a hand-edit removed providers.<id>.oauth_token
When it happens
Trigger: A refresh is scheduled/triggered for a provider that was authenticated with a plain API key (api_key set, no OAuth token), or the token field was removed from the config (hand edit, migration, or partial write) while refresh bookkeeping still references the provider.
Common situations: Switching a provider from OAuth login to a static API key while a refresh timer is still alive; manually editing the config to strip providers.<id>.oauth_token; a scope mismatch where the token was saved to a different config scope than the one being refreshed.
Related errors
- mcp '%s' does not use OAuth authentication
- provider %s not found
- failed to refresh OAuth token for provider %s: %w
- OAuth refresh not supported for provider %s
- failed to configure providers during reload: %w
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/1aba8e48f0c49b4f.
Report an issue: GitHub.