charmbracelet/crush · error

failed to save api key to config file: %w

Error message

failed to save api key to config file: %w

What it means

SetProviderAPIKey persists a credential (string API key or OAuth token) for a provider into the config file via SetConfigField. This error wraps any failure of that underlying write — which itself may be a lock, read, directory, or sjson error — so it reports 'could not save the credential to disk'.

Source

Thrown at internal/config/store.go:572

// SetTransparentBackground sets the transparent background setting and persists it.
func (s *ConfigStore) SetTransparentBackground(scope Scope, enabled bool) error {
	return s.update(scope, func(c *Config) map[string]any {
		c.ensureTUI().Transparent = &enabled
		return map[string]any{"options.tui.transparent": enabled}
	})
}

// SetProviderAPIKey sets the API key for a provider and persists it.
func (s *ConfigStore) SetProviderAPIKey(scope Scope, providerID string, apiKey any) error {
	var providerConfig ProviderConfig
	var exists bool
	var setKeyOrToken func()

	switch v := apiKey.(type) {
	case string:
		if err := s.SetConfigField(scope, fmt.Sprintf("providers.%s.api_key", providerID), v); err != nil {
			return fmt.Errorf("failed to save api key to config file: %w", err)
		}
		setKeyOrToken = func() { providerConfig.APIKey = v }
	case *oauth.Token:
		// Hold the refresh lock across the write so a peer's in-flight
		// token exchange cannot land on top of a credential the user just
		// obtained interactively — which would silently invalidate the
		// login they only just completed.
		if err := s.withRefreshLock(providerID, func() error {
			return s.SetConfigFields(scope, map[string]any{
				fmt.Sprintf("providers.%s.api_key", providerID): v.AccessToken,
				fmt.Sprintf("providers.%s.oauth", providerID):   v,
			})
		}); err != nil {
			return err
		}
		setKeyOrToken = func() {
			providerConfig.APIKey = v.AccessToken
			providerConfig.OAuthToken = v

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Inspect the wrapped cause (%w) — fix the underlying lock/read/JSON error first
  2. Ensure the config directory is writable by the current user
  3. Verify the config file is valid JSON before authenticating
  4. Retry if the failure was transient lock contention

Example fix

// before
if err := store.SetProviderAPIKey(ctx, scope, providerID, key); err != nil {
    return err // credential possibly not persisted; auth state unclear
}
// after
if err := store.SetProviderAPIKey(ctx, scope, providerID, key); err != nil {
    return fmt.Errorf("authentication succeeded but key was NOT saved: %w (check config dir permissions)", err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

if apiKey == "" {
    return errors.New("refusing to save empty api key")
}
if !configDirWritable(filepath.Dir(configPath)) {
    return errors.New("config directory not writable; key will not persist")
}

Type guard

func isCredentialSaveError(err error) bool {
    return err != nil && strings.Contains(err.Error(), "failed to save api key to config file")
}

Try / catch

if err := store.SetProviderAPIKey(ctx, scope, providerID, key); err != nil {
    // auth succeeded but persistence failed — surface prominently
    return fmt.Errorf("credential NOT saved, re-auth will be required: %w", err)
}

Prevention

When it happens

Trigger: Calling SetProviderAPIKey with a string apiKey when the underlying atomicWrite fails: config directory not writable, flock contention/deadline, unreadable config file, or sjson.Set failure on providers.<id>.api_key.

Common situations: Read-only or permission-denied config directory; another crush instance holds the config lock; malformed JSON in the config file; provider ID containing characters that break the sjson path.

Related errors


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