ruvnet/ruflo · error · ScopeConsentMismatchError

profile " " has scope(s) without matching local consent: —…

Error message

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

What it means

getValidAccessToken() throws ScopeConsentMismatchError when the stored profile requests scopes whose domain (via domainForScope) has no matching local consent record (hasConsent is false). Authenticated capability is denied until local consent exists for every requested scope domain — an intentional gate that remote grants alone cannot bypass.

Solutions

  1. Re-run the login/consent flow to record consent for the listed domains: ruflo auth login --profile <profile>
  2. Inspect the profile's scopes (ruflo auth status) and confirm each listed scope is still needed
  3. If a scope is unwanted, re-login requesting only the scopes you will consent to
  4. If consent was declined by mistake, clear/re-answer the funnel consent for that domain and retry
Defensive patterns

Strategy: validation

Validate before calling

import { hasConsent } from '../funnel/index.js';
import { domainForScope } from '../auth/scopes.js';
const missing = profile.scopes.filter((s) => {
  const d = domainForScope(s);
  return d !== undefined && !hasConsent(d);
});
if (missing.length > 0) await rerunConsentFlow(profile.name, missing);

Type guard

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

Try / catch

try {
  token = await getValidAccessToken(profileName);
} catch (e) {
  if (isScopeConsentMismatch(e)) {
    // e.message lists the unconsented scopes — route the user through consent/login
    await promptConsent(e.message);
    process.exit(4);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling getValidAccessToken() for a profile whose scopes include a domain whose local consent was never recorded, was revoked, or whose consent store was cleared; also when new scopes were added to the profile without re-running the local consent flow.

Common situations: Consent store reset when funnel state was wiped; a profile restored/copied from another machine where consent answers differed; product added a new scope domain and the old login predates it; user declined consent earlier and the denial was remembered.

Understand the failure class

Related errors


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

Appendix: 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 fa13ee4ad6)