{"record":{"id":"846995ed30849d23","repo":"charmbracelet/crush","slug":"failed-to-persist-refreshed-token-w","errorCode":null,"errorMessage":"failed to persist refreshed token: %w","messagePattern":"failed to persist refreshed token: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/config/store.go","lineNumber":746,"sourceCode":"\t\t\t}\n\t\t\tslog.Info(\"Retrying exchange with refresh token rotated by another session\", \"provider\", providerID)\n\t\t\trefreshedToken, refreshErr = s.exchange(ctx, providerID, diskToken.RefreshToken)\n\t\t}\n\t}\n\tif refreshErr != nil {\n\t\treturn fmt.Errorf(\"failed to refresh OAuth token for provider %s: %w\", providerID, refreshErr)\n\t}\n\n\tslog.Info(\"Successfully refreshed OAuth token\", \"provider\", providerID)\n\tif err := s.applyToken(providerConfig, refreshedToken, providerID); err != nil {\n\t\treturn err\n\t}\n\n\tif err := s.SetConfigFields(scope, map[string]any{\n\t\tfmt.Sprintf(\"providers.%s.api_key\", providerID): refreshedToken.AccessToken,\n\t\tfmt.Sprintf(\"providers.%s.oauth\", providerID):   refreshedToken,\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"failed to persist refreshed token: %w\", err)\n\t}\n\treturn nil\n}\n\n// WaitForTokenChange blocks until SignalAuthComplete is called for the\n// given provider or the context is cancelled. It is used by OnAuthRefresh\n// callbacks to wait for interactive re-authentication to complete before\n// retrying a failed request. The channel is created atomically with the\n// wait registration so a concurrent SignalAuthComplete cannot miss it.\nfunc (s *ConfigStore) WaitForTokenChange(ctx context.Context, providerID string) error {\n\ts.authSignalMu.Lock()\n\tch, ok := s.authSignals[providerID]\n\tif !ok {\n\t\tch = make(chan struct{})\n\t\tif s.authSignals == nil {\n\t\t\ts.authSignals = make(map[string]chan struct{})\n\t\t}\n\t\ts.authSignals[providerID] = ch","sourceCodeStart":728,"sourceCodeEnd":764,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/config/store.go#L728-L764","documentation":"After a successful token refresh, the store writes the new access token and oauth struct back to config via SetConfigFields. If persisting fails, the refreshed token is lost (and the rotated refresh token may only exist in memory), so this wraps the persistence error. A failure here risks the on-disk refresh token no longer being valid on the provider side.","triggerScenarios":"SetConfigFields(scope, {providers.<id>.api_key, providers.<id>.oauth}) returns an error: config file not writable, disk full, JSON marshalling of the refreshed token fails, or the provider entry was removed concurrently.","commonSituations":"Read-only config directory or file owned by another user; another process truncated/locked the config file; token struct contains fields that fail marshalling.","solutions":["Fix permissions on the config file/directory so the process can write it.","Free disk space if the write failed due to ENOSPC.","Re-run the refresh after fixing persistence so a consistent token pair is stored.","Back up the config file and retry; if it keeps failing, re-authenticate from scratch."],"exampleFix":"// before\nif err := s.SetConfigFields(scope, fields); err != nil {\n    return fmt.Errorf(\"failed to persist refreshed token: %w\", err)\n}\n// after: check writability first\nif err := checkConfigWritable(s.workingDir); err != nil {\n    return fmt.Errorf(\"config dir not writable, cannot persist token: %w\", err)\n}","handlingStrategy":"validation","validationCode":"info, err := os.Stat(configPath); err == nil && info.Mode().Perm()&0o200 != 0 // writable?","typeGuard":null,"tryCatchPattern":"if err := refresh(); err != nil {\n    var perr *PersistError\n    if errors.As(err, &perr) { freeSpaceOrFixPerms(); retryRefresh() }\n}","preventionTips":["Ensure config file/dir write permissions for the running user","Monitor disk space on the config volume","Back up the config before auth flows"],"tags":["oauth","persistence","filesystem"],"backgroundTag":"config-write-failed","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}