{"record":{"id":"285285c16b8ef814","repo":"charmbracelet/crush","slug":"failed-to-save-api-key-to-config-file-w","errorCode":null,"errorMessage":"failed to save api key to config file: %w","messagePattern":"failed to save api key to config file: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/config/store.go","lineNumber":572,"sourceCode":"\n// SetTransparentBackground sets the transparent background setting and persists it.\nfunc (s *ConfigStore) SetTransparentBackground(scope Scope, enabled bool) error {\n\treturn s.update(scope, func(c *Config) map[string]any {\n\t\tc.ensureTUI().Transparent = &enabled\n\t\treturn map[string]any{\"options.tui.transparent\": enabled}\n\t})\n}\n\n// SetProviderAPIKey sets the API key for a provider and persists it.\nfunc (s *ConfigStore) SetProviderAPIKey(scope Scope, providerID string, apiKey any) error {\n\tvar providerConfig ProviderConfig\n\tvar exists bool\n\tvar setKeyOrToken func()\n\n\tswitch v := apiKey.(type) {\n\tcase string:\n\t\tif err := s.SetConfigField(scope, fmt.Sprintf(\"providers.%s.api_key\", providerID), v); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to save api key to config file: %w\", err)\n\t\t}\n\t\tsetKeyOrToken = func() { providerConfig.APIKey = v }\n\tcase *oauth.Token:\n\t\t// Hold the refresh lock across the write so a peer's in-flight\n\t\t// token exchange cannot land on top of a credential the user just\n\t\t// obtained interactively — which would silently invalidate the\n\t\t// login they only just completed.\n\t\tif err := s.withRefreshLock(providerID, func() error {\n\t\t\treturn s.SetConfigFields(scope, map[string]any{\n\t\t\t\tfmt.Sprintf(\"providers.%s.api_key\", providerID): v.AccessToken,\n\t\t\t\tfmt.Sprintf(\"providers.%s.oauth\", providerID):   v,\n\t\t\t})\n\t\t}); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsetKeyOrToken = func() {\n\t\t\tproviderConfig.APIKey = v.AccessToken\n\t\t\tproviderConfig.OAuthToken = v","sourceCodeStart":554,"sourceCodeEnd":590,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/config/store.go#L554-L590","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nif err := store.SetProviderAPIKey(ctx, scope, providerID, key); err != nil {\n    return err // credential possibly not persisted; auth state unclear\n}\n// after\nif err := store.SetProviderAPIKey(ctx, scope, providerID, key); err != nil {\n    return fmt.Errorf(\"authentication succeeded but key was NOT saved: %w (check config dir permissions)\", err)\n}","handlingStrategy":"try-catch","validationCode":"if apiKey == \"\" {\n    return errors.New(\"refusing to save empty api key\")\n}\nif !configDirWritable(filepath.Dir(configPath)) {\n    return errors.New(\"config directory not writable; key will not persist\")\n}","typeGuard":"func isCredentialSaveError(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"failed to save api key to config file\")\n}","tryCatchPattern":"if err := store.SetProviderAPIKey(ctx, scope, providerID, key); err != nil {\n    // auth succeeded but persistence failed — surface prominently\n    return fmt.Errorf(\"credential NOT saved, re-auth will be required: %w\", err)\n}","preventionTips":["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"],"tags":["config-write","api-key","persistence"],"backgroundTag":"config-write-failed","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}