charmbracelet/crush · error
failed to persist refreshed token: %w
Error message
failed to persist refreshed token: %w
What it means
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.
Source
Thrown at internal/config/store.go:746
}
slog.Info("Retrying exchange with refresh token rotated by another session", "provider", providerID)
refreshedToken, refreshErr = s.exchange(ctx, providerID, diskToken.RefreshToken)
}
}
if refreshErr != nil {
return fmt.Errorf("failed to refresh OAuth token for provider %s: %w", providerID, refreshErr)
}
slog.Info("Successfully refreshed OAuth token", "provider", providerID)
if err := s.applyToken(providerConfig, refreshedToken, providerID); err != nil {
return err
}
if err := s.SetConfigFields(scope, map[string]any{
fmt.Sprintf("providers.%s.api_key", providerID): refreshedToken.AccessToken,
fmt.Sprintf("providers.%s.oauth", providerID): refreshedToken,
}); err != nil {
return fmt.Errorf("failed to persist refreshed token: %w", err)
}
return nil
}
// WaitForTokenChange blocks until SignalAuthComplete is called for the
// given provider or the context is cancelled. It is used by OnAuthRefresh
// callbacks to wait for interactive re-authentication to complete before
// retrying a failed request. The channel is created atomically with the
// wait registration so a concurrent SignalAuthComplete cannot miss it.
func (s *ConfigStore) WaitForTokenChange(ctx context.Context, providerID string) error {
s.authSignalMu.Lock()
ch, ok := s.authSignals[providerID]
if !ok {
ch = make(chan struct{})
if s.authSignals == nil {
s.authSignals = make(map[string]chan struct{})
}
s.authSignals[providerID] = chView on GitHub (pinned to 7944b8e522)
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.
Example fix
// before
if err := s.SetConfigFields(scope, fields); err != nil {
return fmt.Errorf("failed to persist refreshed token: %w", err)
}
// after: check writability first
if err := checkConfigWritable(s.workingDir); err != nil {
return fmt.Errorf("config dir not writable, cannot persist token: %w", err)
} Defensive patterns
Strategy: validation
Validate before calling
info, err := os.Stat(configPath); err == nil && info.Mode().Perm()&0o200 != 0 // writable?
Try / catch
if err := refresh(); err != nil {
var perr *PersistError
if errors.As(err, &perr) { freeSpaceOrFixPerms(); retryRefresh() }
} Prevention
- Ensure config file/dir write permissions for the running user
- Monitor disk space on the config volume
- Back up the config before auth flows
When it happens
Trigger: 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.
Common situations: Read-only config directory or file owned by another user; another process truncated/locked the config file; token struct contains fields that fail marshalling.
Related errors
- docker MCP started but failed to persist configuration: %w
- failed to persist docker mcp configuration: %w
- failed to update preferred large model: %w
- failed to update preferred small model: %w
- failed to save providers to cache: %w
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/846995ed30849d23.
Report an issue: GitHub.