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] = ch

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Fix permissions on the config file/directory so the process can write it.
  2. Free disk space if the write failed due to ENOSPC.
  3. Re-run the refresh after fixing persistence so a consistent token pair is stored.
  4. 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

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


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