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 = vView on GitHub (pinned to 7944b8e522)
Solutions
- Inspect the wrapped cause (%w) — fix the underlying lock/read/JSON error first
- Ensure the config directory is writable by the current user
- Verify the config file is valid JSON before authenticating
- 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
- Check config dir writability before starting OAuth/login flows
- Warn users prominently when auth succeeds but persistence fails
- Validate the provider ID contains no sjson-breaking characters before saving
- Keep config file ownership/permissions correct to avoid cascading write errors
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
- not a valid bedrock api key
- not a valid vercel api key
- failed to save todos: %w
- failed to create session for non-interactive mode: %w
- failed to get last assistant message: %w
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/285285c16b8ef814.
Report an issue: GitHub.