ruvnet/ruflo · error · SessionOnlyExpiredError

profile " " has no persisted refresh token and its…

Error message

profile "${profile}" has no persisted refresh token and its in-memory access token is absent or expiring — run: ruflo auth login --profile ${profile}

What it means

SessionOnlyExpiredError thrown at the first site in getValidAccessToken(): the profile exists but has no keychainRef, meaning no refresh token was ever persisted (session-only login), and the in-memory access token is absent or inside the 60-second refresh window. There is no way to mint a token without user interaction, so the message directs to `ruflo auth login`.

Solutions

  1. Re-login with a durable credential: ruflo auth login --profile <profile>
  2. If using --token-stdin, include a refresh_token in the JSON so a keychainRef gets persisted
  3. For CI, provision a fresh access token per job instead of expecting it to survive process restarts
  4. Log in via the interactive PKCE flow, which always persists a refresh token in the OS keychain

Example fix

// before
echo '{"access_token":"eyJ...","expires_in":3600}' | ruflo auth login --token-stdin
// after (include refresh_token so the credential survives restarts)
echo '{"access_token":"eyJ...","refresh_token":"rt_...","expires_in":3600}' | ruflo auth login --token-stdin
Defensive patterns

Strategy: try-catch

Validate before calling

// Before relying on a profile across restarts, ensure it persists a refresh token
const prof = await loadProfileState(profileName);
if (!prof?.keychainRef && !process.env.RUFLO_EPHEMERAL) {
  warn('session-only login detected; it will not survive process restarts');
}

Type guard

import { SessionOnlyExpiredError } from '@claude-flow/cli/dist/auth/client.js';
function isSessionOnlyExpired(e: unknown): e is SessionOnlyExpiredError {
  return e instanceof Error && e.name === 'SessionOnlyExpiredError';
}

Try / catch

try {
  token = await getValidAccessToken(profileName);
} catch (e) {
  if (isSessionOnlyExpired(e)) {
    // message already carries the re-login command; surface it and stop
    console.error(e.message);
    process.exit(3);
  }
  throw e;
}

Prevention

When it happens

Trigger: Logging in via `--token-stdin` with only an access_token (no refresh_token), then letting that token expire (or restarting the process so the memory cache is gone) before calling getValidAccessToken(); any session-only profile after process restart.

Common situations: Short-lived CI tokens piped via stdin: the process restarts between pipeline jobs and the token is gone; a user who logged in with an opaque token from a vault that provides no refresh token; expiry window reached during a long-running command.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/37924aa2c4174250. Report an issue: GitHub.

Appendix: source

Thrown at v3/@claude-flow/cli/src/auth/client.ts:251

 * token, then update metadata and the process cache. Refresh is deliberately
 * demand-driven: offline-safe commands such as plain `auth status` never call
 * this function and therefore never create background traffic or retry loops.
 */
export async function getValidAccessToken(profileName = 'default'): Promise<string> {
  const profile = getProfile(profileName);
  if (!profile) throw new NotLoggedInError(profileName);

  const scopesWithoutConsent = profile.scopes.filter((scope) => {
    const domain = domainForScope(scope);
    return domain !== undefined && !hasConsent(domain);
  });
  if (scopesWithoutConsent.length > 0) {
    throw new ScopeConsentMismatchError(profileName, scopesWithoutConsent);
  }

  const cached = getSessionToken(profileName, ACCESS_TOKEN_REFRESH_WINDOW_MS);
  if (cached) return cached;
  if (!profile.keychainRef) throw new SessionOnlyExpiredError(profileName);

  const sec = await loadSecurityOAuth();
  const keychain = await sec.createKeychainAdapter();
  const refreshTokenValue = await keychain.getSecret(KEYCHAIN_SERVICE, profile.keychainRef);
  if (!refreshTokenValue) throw new SessionOnlyExpiredError(profileName);

  const refreshed = await refreshAccessToken(refreshTokenValue);
  if (!refreshed.access_token) throw new Error('Cognitum refresh response did not contain an access token');

  // Cognitum rotates refresh tokens with reuse detection. Commit the rotated
  // credential first; if this write fails, do not publish/cache the access
  // token and do not retry the already-spent old refresh token here.
  if (refreshed.refresh_token) {
    await keychain.setSecret(KEYCHAIN_SERVICE, profile.keychainRef, refreshed.refresh_token);
  }

  const expiresAtMs = Date.now() + Math.max(0, refreshed.expires_in ?? 0) * 1000;
  setSessionToken(profileName, refreshed.access_token, expiresAtMs);

View on GitHub (pinned to fa13ee4ad6)