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 a

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Re-add the provider to the config (crush login <provider>) to restore its OAuth token
  2. Confirm the provider ID used matches the one in the config file exactly
  3. Reload/restart so in-memory config matches the file, then retry the request
  4. 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

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


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/cb9a1c3047d5fe53. Report an issue: GitHub.