ruvnet/ruflo · error · ScopeConsentMismatchError

profile "${profile}" has scope(s) without matching local con

Error message

profile "${profile}" has scope(s) without matching local consent: ${scopes.join(', ')} — authenticated capability denied

What it means

Thrown by getValidAccessToken (ScopeConsentMismatchError) when the profile's scopes include one or more domains for which no local consent record exists. Consent is a local gate: even with a valid token, scopes lacking consent are denied.

Source

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

 * Returns an access token suitable for an authenticated call.
 *
 * Fast path: a process-memory token with more than one minute remaining.
 * Slow path: load the profile's refresh token from the OS keychain, perform
 * one refresh, persist a rotated refresh token BEFORE exposing the new access
 * 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) {

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Re-establish consent for each missing domain (e.g. ruflo auth consent <domain>), or re-login.
  2. Drop the unconsented scope from the profile if it is no longer needed.
Defensive patterns

Strategy: validation

Validate before calling

const missing = profile.scopes.filter(s => {
  const d = domainForScope(s);
  return d !== undefined && !hasConsent(d);
});
if (missing.length > 0) {
  throw new Error(`Missing consent for: ${missing.join(', ')}`);
}

Prevention

When it happens

Trigger: Profile exists but its scopes include a domain with no consent record: consent expired/revoked, a scope added to the profile after login, or a consent store reset.

Common situations: Consent store cleared; profile imported between machines without consent; scopes changed server-side after the original login.

Understand the failure class

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/97c85287d2471608. Report an issue: GitHub.