ruvnet/ruflo · error · StateMismatchError
state mismatch — the OAuth callback did not match the reques
Error message
state mismatch — the OAuth callback did not match the request this CLI sent
What it means
Thrown by the interactive PKCE login (StateMismatchError) when the state returned by the OAuth callback does not equal the state the CLI generated and embedded in the authorize URL. This is CSRF protection firing: the callback did not correspond to this CLI's outbound request.
Source
Thrown at v3/@claude-flow/cli/src/auth/client.ts:132
}
}
/** Browser-based loopback PKCE login — the ADR-306 default for an interactive desktop. */
export async function browserLogin(print: (line: string) => void): Promise<LoginResult> {
const sec = await loadSecurityOAuth();
const server = await sec.CallbackServer.bind();
const pkce = sec.generatePkce();
const url = sec.authorizeUrl(server.redirectUri, pkce.state, pkce.codeChallenge);
print('Opening your browser to sign in to Cognitum...');
print(`If it doesn't open automatically, visit:\n\n ${url}\n`);
await sec.openBrowser(url).catch(() => {}); // best-effort — the URL above is always the fallback
print('Waiting for you to finish signing in...');
const result = await server.awaitCallback();
const validated = validateCallback(result.error, result.code, result.state, pkce.state);
if (!validated.ok) {
if (validated.reason === 'state-mismatch') throw new StateMismatchError();
throw new LoginDeniedError(validated.detail ?? 'unknown');
}
const tokens = await sec.exchangeCode(validated.code, pkce.codeVerifier, server.redirectUri);
return { tokens, method: 'pkce' };
}
/** Headless fallback: prints the authorize URL with the OOB redirect, prompts for the pasted code. */
export async function manualLogin(
print: (line: string) => void,
input: NodeJS.ReadableStream = process.stdin,
): Promise<LoginResult> {
const sec = await loadSecurityOAuth();
print('Browser-based callback unavailable (SSH/container detected, or --no-browser).\n');
const pkce = sec.generatePkce();
const url = sec.authorizeUrl(sec.OOB_REDIRECT_URI, pkce.state, pkce.codeChallenge);
print(`Open this URL in a browser and authorize:\n\n ${url}\n`);View on GitHub (pinned to 6b01dc5a68)
Solutions
- Close all stale login tabs/windows and start a single fresh `ruflo auth login`.
- Ensure no second process is running a login concurrently for the same profile.
- Verify the provider's redirect_uri matches the CLI's loopback callback.
Defensive patterns
Strategy: try-catch
Type guard
function isStateMismatch(e: unknown): boolean {
return e instanceof Error && /state mismatch|did not match the request/.test(e.message);
} Try / catch
try {
await interactiveLogin(print);
} catch (e) {
if (isStateMismatch(e)) {
// restart the login once with a fresh PKCE pair
}
throw e;
} Prevention
- Do not run concurrent logins for the same profile.
- Close prior authorize tabs before starting a new login.
- Match the provider redirect_uri to the CLI loopback callback exactly.
When it happens
Trigger: The provider redirects back with a state that differs from pkce.state: a stale authorize tab from a previous login, a second concurrent login, a replayed/intercepted redirect, or a misconfigured redirect_uri.
Common situations: User completed an older login tab; two `ruflo auth login` runs in parallel; provider redirect_uri does not match the CLI callback server.
Related errors
- login cancelled: no code was entered
- --token-stdin: no input received on stdin
- --token-stdin expects a single JSON object: {"access_token",
- --token-stdin: JSON is missing required field "access_token"
- Could not reach the Cognitum auth service. ruflo core functi
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/1ba2a262fee66197.
Report an issue: GitHub.