{"record":{"id":"ed4ecb6e17f20418","repo":"ruvnet/ruflo","slug":"login-cancelled-no-code-was-entered","errorCode":null,"errorMessage":"login cancelled: no code was entered","messagePattern":"login cancelled: no code was entered","errorType":"exception","errorClass":"LoginCancelledError","httpStatus":null,"severity":"info","filePath":"v3/@claude-flow/cli/src/auth/client.ts","lineNumber":165,"sourceCode":"\n  const pkce = sec.generatePkce();\n  const url = sec.authorizeUrl(sec.OOB_REDIRECT_URI, pkce.state, pkce.codeChallenge);\n  print(`Open this URL in a browser and authorize:\\n\\n  ${url}\\n`);\n\n  // `rl.question()` resolves on a newline-terminated 'line' event — if `input`\n  // ends without ever emitting one (e.g. stdin closed early, or piped input\n  // with no trailing newline), it hangs forever rather than treating EOF as\n  // a cancellation. Race it against the interface's own 'close' event so an\n  // early EOF resolves to \"\" (-> LoginCancelledError below) instead of hanging.\n  const rl = readline.createInterface({ input, terminal: false });\n  let code: string;\n  try {\n    const closed = new Promise<string>((resolve) => rl.once('close', () => resolve('')));\n    code = (await Promise.race([rl.question('Paste the code shown after authorizing: '), closed])).trim();\n  } finally {\n    rl.close();\n  }\n  if (!code) throw new LoginCancelledError();\n\n  const tokens = await sec.exchangeManualCode(code, pkce.codeVerifier);\n  return { tokens, method: 'device' };\n}\n\n/**\n * `--token-stdin`: reads one JSON object from stdin,\n * `{access_token, refresh_token?, expires_in, scope}`. Wire format is not\n * specified by ADR-306 — defined here as typed JSON rather than a bare\n * token string, so scope/expiry are explicit rather than inferred.\n */\nexport async function tokenStdinLogin(input: NodeJS.ReadableStream = process.stdin): Promise<LoginResult> {\n  const chunks: Buffer[] = [];\n  for await (const chunk of input) chunks.push(chunk as Buffer);\n  const raw = Buffer.concat(chunks).toString('utf-8').trim();\n  if (!raw) throw new Error('--token-stdin: no input received on stdin');\n\n  let parsed: { access_token?: string; refresh_token?: string; expires_in?: number; scope?: string };","sourceCodeStart":147,"sourceCodeEnd":183,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/cli/src/auth/client.ts#L147-L183","documentation":"LoginCancelledError fires only in the manual (OOB paste-the-code) flow: manualLogin prompted 'Paste the code shown after authorizing:' and got an empty line — the user pressed Enter on nothing, or stdin hit EOF (the readline interface is explicitly raced against 'close' so early EOF resolves to '' instead of hanging). It represents an intentional or environment-driven cancellation, not a server failure.","triggerScenarios":"manualLogin(print, input) where input closes before any code is typed: pressing Enter at the empty prompt, Ctrl-D/Ctrl-C terminating stdin, a piped stdin with no content (manualLogin </dev/null), or scripts invoking the manual flow non-interactively without supplying the code.","commonSituations":"Headless/SSH sessions where the manual fallback triggers but no TTY input is available; automation piping empty stdin; users confused by the prompt and hitting Enter; CI jobs accidentally choosing the device/manual path.","solutions":["If interactive: re-run `ruflo auth login`, open the printed URL, and paste the code at the prompt","If scripted: feed the code via stdin (echo \"$CODE\" | ruflo auth login --manual or the equivalent programmatic manualLogin call) — or better, use --token-stdin with a JSON token","Catch LoginCancelledError and exit with a benign code (0/130) — cancellation is not an error state for the user","Ensure the environment isn't headless-triggering the manual flow when a browser is actually available (isProbablyHeadless false-negatives)"],"exampleFix":"// before — cancellation treated as a crash\nconst result = await manualLogin(print);\n\n// after — graceful exit on cancellation\ntry {\n  const result = await manualLogin(print);\n} catch (e) {\n  if (e instanceof LoginCancelledError) { print('Login cancelled.'); process.exit(130); }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"// scripted manual login: provide the code on stdin so the prompt never sees EOF\nconst result = await manualLogin(print, Readable.from([`${code}\\n`]));","typeGuard":null,"tryCatchPattern":"import { LoginCancelledError } from './auth/client.js';\n\ntry { await manualLogin(print); }\ncatch (e) {\n  if (e instanceof LoginCancelledError) { process.exit(130); /* benign cancel */ }\n  throw e;\n}","preventionTips":["When scripting the manual flow, pipe the code in rather than relying on a TTY","Treat cancellation as exit code 130/0, not a failure","Ensure headless detection doesn't force the manual path when a browser exists"],"tags":["oauth","auth","user-cancellation","cli"],"backgroundTag":"login-cancelled-by-user","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"}