charmbracelet/crush · error
OAuth refresh not supported for provider %s
Error message
OAuth refresh not supported for provider %s
What it means
exchange dispatches token refresh to a provider-specific implementation. Only Copilot (via copilot.RefreshToken) and Hyper (via hyper.ExchangeToken) are supported; any other providerID hits the default branch and returns this error. It means the config store cannot refresh OAuth credentials for that provider.
Source
Thrown at internal/config/store.go:873
return nil
}
return diskToken
}
// exchange performs the provider-specific OAuth token exchange. Tests may
// override it via the exchangeToken field; production uses the real
// provider clients.
func (s *ConfigStore) exchange(ctx context.Context, providerID, refreshToken string) (*oauth.Token, error) {
if s.exchangeToken != nil {
return s.exchangeToken(ctx, providerID, refreshToken)
}
switch providerID {
case string(catwalk.InferenceProviderCopilot):
return copilot.RefreshToken(ctx, refreshToken)
case hyperp.Name:
return hyper.ExchangeToken(ctx, refreshToken)
default:
return nil, fmt.Errorf("OAuth refresh not supported for provider %s", providerID)
}
}
// withRefreshLock runs fn while holding the per-provider cross-process
// refresh lock, so a credential write cannot interleave with a peer's
// token exchange. Acquisition is best effort: when the lock cannot be
// taken in time, fn runs anyway rather than blocking a write the user is
// waiting on.
func (s *ConfigStore) withRefreshLock(providerID string, fn func() error) error {
ctx, cancel := context.WithTimeout(context.Background(), credentialWriteLockDeadline)
defer cancel()
release, err := lock.File(ctx, s.refreshLockPath(providerID))
if err != nil {
slog.Warn("Writing credentials without the refresh lock", "provider", providerID, "error", err)
return fn()
}
defer release()
return fn()View on GitHub (pinned to 7944b8e522)
Solutions
- Use a supported provider (Copilot or Hyper) for OAuth refresh, or supply a static API key for the unsupported provider.
- Remove the stale oauth entry for the unsupported provider so the store stops trying to refresh it.
- Update Crush / catwalk provider definitions to a version that supports the provider's refresh flow.
Example fix
// before: oauth block on unsupported provider
providers.monster.oauth = { ... }
// after: static key instead
providers.monster.api_key = "sk-..." Defensive patterns
Strategy: validation
Validate before calling
supported := map[string]bool{"copilot": true, "hyper": true}
if !supported[providerID] && hasOAuthBlock(providerID) { removeOAuthBlock(providerID) } Try / catch
if err := refresh(); err != nil && strings.Contains(err.Error(), "not supported") {
fallbackToAPIKey(providerID)
} Prevention
- Only configure oauth blocks for providers with refresh support
- Use static api_key for other providers
- Keep provider definitions updated with the app version
When it happens
Trigger: refreshOAuthTokenLocked calls s.exchange for a providerID other than catwalk.InferenceProviderCopilot or hyperp.Name — i.e. a provider configured with OAuth-style fields but lacking a refresh implementation.
Common situations: User hand-edited crush config adding an oauth block for an unsupported provider; provider support was removed/renamed in a version change; typo in provider ID.
Related errors
- mcp '%s' does not use OAuth authentication
- provider %s does not have an OAuth token
- failed to configure providers during reload: %w
- requested channels differ from the existing workspace; chann
- github copilot not available
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/b1111dac8f380fa4.
Report an issue: GitHub.