charmbracelet/crush · error
provider %s not found
Error message
provider %s not found
What it means
refreshOAuthTokenLocked refreshes an expired OAuth token for a provider. Before doing any network work it looks the provider up in the live in-memory config; if no provider with that ID is configured it returns this error, since there is nothing to refresh.
Source
Thrown at internal/config/store.go:673
// read-decide-exchange-write cycle, so only one process exchanges at a
// time. A process that acquires the lock after a peer rotated finds the
// peer's fresh token on disk and adopts it instead of exchanging.
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 aView on GitHub (pinned to 7944b8e522)
Solutions
- Re-add the provider to the config (crush login <provider>) to restore its OAuth token
- Confirm the provider ID used matches the one in the config file exactly
- Reload/restart so in-memory config matches the file, then retry the request
- If the provider was intentionally removed, clear any cached clients referencing it
Example fix
// before
resp, err := client.Do(ctx, req) // internally triggers refresh for "copilot"
// after
if _, ok := store.Config().Providers.Get(providerID); !ok {
return nil, fmt.Errorf("provider %q not configured; run 'crush login %s' first", providerID, providerID)
}
resp, err := client.Do(ctx, req) Defensive patterns
Strategy: validation
Validate before calling
if _, ok := store.Config().Providers.Get(providerID); !ok {
return fmt.Errorf("provider %q is not configured; skipping token refresh", providerID)
} Type guard
func providerConfigured(store *config.ConfigStore, id string) bool {
_, ok := store.Config().Providers.Get(id)
return ok
} Try / catch
if err := doRequest(ctx); err != nil {
if strings.Contains(err.Error(), "provider ") && strings.Contains(err.Error(), "not found") {
return fmt.Errorf("provider removed from config; re-run 'crush login' to re-authenticate: %w", err)
}
return err
} Prevention
- Cancel pending token refresh timers when removing a provider from config
- Match provider IDs exactly (case/spelling) between config and API calls
- Reload the store after config file edits so in-memory state matches disk
- Re-authenticate instead of retrying when the provider was intentionally deleted
When it happens
Trigger: The background refresh path (invoked via refreshSF with single-flight per provider) fires for a providerID that was removed from the config (user deleted it, config file reloaded without it) or never existed / is misspelled.
Common situations: Provider removed from crush.json while a token refresh was pending; config reloaded from disk dropping the provider between auth and refresh; referencing a provider by an ID that differs in case or spelling from the configured one.
Related errors
- provider %s does not have an OAuth token
- failed to refresh OAuth token for provider %s: %w
- copilot token request failed: %s - %s
- github copilot not available
- ${ErrorDescription}
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/cb9a1c3047d5fe53.
Report an issue: GitHub.