{"record":{"id":"97c85287d2471608","repo":"ruvnet/ruflo","slug":"profile-profile-has-scope-s-without-matching","errorCode":null,"errorMessage":"profile \"${profile}\" has scope(s) without matching local consent: ${scopes.join(', ')} — authenticated capability denied","messagePattern":"profile \"(.+?)\" has scope\\(s\\) without matching local consent: (.+?) — authenticated capability denied","errorType":"exception","errorClass":"ScopeConsentMismatchError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/auth/client.ts","lineNumber":246,"sourceCode":" * Returns an access token suitable for an authenticated call.\n *\n * Fast path: a process-memory token with more than one minute remaining.\n * Slow path: load the profile's refresh token from the OS keychain, perform\n * one refresh, persist a rotated refresh token BEFORE exposing the new access\n * token, then update metadata and the process cache. Refresh is deliberately\n * demand-driven: offline-safe commands such as plain `auth status` never call\n * this function and therefore never create background traffic or retry loops.\n */\nexport async function getValidAccessToken(profileName = 'default'): Promise<string> {\n  const profile = getProfile(profileName);\n  if (!profile) throw new NotLoggedInError(profileName);\n\n  const scopesWithoutConsent = profile.scopes.filter((scope) => {\n    const domain = domainForScope(scope);\n    return domain !== undefined && !hasConsent(domain);\n  });\n  if (scopesWithoutConsent.length > 0) {\n    throw new ScopeConsentMismatchError(profileName, scopesWithoutConsent);\n  }\n\n  const cached = getSessionToken(profileName, ACCESS_TOKEN_REFRESH_WINDOW_MS);\n  if (cached) return cached;\n  if (!profile.keychainRef) throw new SessionOnlyExpiredError(profileName);\n\n  const sec = await loadSecurityOAuth();\n  const keychain = await sec.createKeychainAdapter();\n  const refreshTokenValue = await keychain.getSecret(KEYCHAIN_SERVICE, profile.keychainRef);\n  if (!refreshTokenValue) throw new SessionOnlyExpiredError(profileName);\n\n  const refreshed = await refreshAccessToken(refreshTokenValue);\n  if (!refreshed.access_token) throw new Error('Cognitum refresh response did not contain an access token');\n\n  // Cognitum rotates refresh tokens with reuse detection. Commit the rotated\n  // credential first; if this write fails, do not publish/cache the access\n  // token and do not retry the already-spent old refresh token here.\n  if (refreshed.refresh_token) {","sourceCodeStart":228,"sourceCodeEnd":264,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/cli/src/auth/client.ts#L228-L264","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Re-run the login/consent flow to record consent for the listed domains: ruflo auth login --profile <profile>","Inspect the profile's scopes (ruflo auth status) and confirm each listed scope is still needed","If a scope is unwanted, re-login requesting only the scopes you will consent to","If consent was declined by mistake, clear/re-answer the funnel consent for that domain and retry"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import { hasConsent } from '../funnel/index.js';\nimport { domainForScope } from '../auth/scopes.js';\nconst missing = profile.scopes.filter((s) => {\n  const d = domainForScope(s);\n  return d !== undefined && !hasConsent(d);\n});\nif (missing.length > 0) await rerunConsentFlow(profile.name, missing);","typeGuard":"import { ScopeConsentMismatchError } from '@claude-flow/cli/dist/auth/client.js';\nfunction isScopeConsentMismatch(e: unknown): e is ScopeConsentMismatchError {\n  return e instanceof Error && e.name === 'ScopeConsentMismatchError';\n}","tryCatchPattern":"try {\n  token = await getValidAccessToken(profileName);\n} catch (e) {\n  if (isScopeConsentMismatch(e)) {\n    // e.message lists the unconsented scopes — route the user through consent/login\n    await promptConsent(e.message);\n    process.exit(4);\n  }\n  throw e;\n}","preventionTips":["Check hasConsent for each scope domain right after login, not at first authenticated call","When adding new scopes to a profile, budget a re-consent step in the rollout","Don't strip scopes from the error message — users need the list to fix consent"],"tags":["auth","oauth","scopes","consent","authorization"],"backgroundTag":"oauth-scope-not-granted","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}