JuliusBrussee/caveman · warning · Error

device login timed out before approval

Error message

device login timed out before approval

What it means

runCavemanDeviceFlow starts a device-authorization login: it fetches a device code, prints it via onCode, then polls the token endpoint until the user approves in a browser or the code expires (deadline = Date.now() + expires_in*1000). This error is thrown when the while loop exits because the deadline passed without the token endpoint returning an access_token — i.e. the user never completed the approval within the code's lifetime. It is an expected, deliberate terminal state, not an internal failure.

Source

Thrown at packages/device-auth/src/index.ts:172

            client: options.client,
            code,
            credentials,
            fetcher,
            ...(options.signal === undefined ? {} : { signal: options.signal }),
            sleep: wait,
          });
          acknowledged = true;
        },
      };
    }
    const errorCode = typeof payload.error === "string" ? payload.error : "";
    if (errorCode === "slow_down") intervalMs = nextDevicePollIntervalMs(intervalMs, errorCode);
    else if (errorCode !== "" && errorCode !== "authorization_pending") {
      throw new Error(`device login failed: ${errorCode}`);
    }
    await wait(Math.max(intervalMs, 200));
  }
  throw new Error("device login timed out before approval");
}

View on GitHub (pinned to df2ccd85c9)

Solutions

  1. Re-run the login command to get a fresh device code, then open the verification_uri and enter the user_code promptly.
  2. Make sure onCode surfaced the code visibly (open the verification_uri in a browser and enter user_code before expires_in elapses).
  3. Check server-side approval requirements (admin consent, correct org) that might silently block the token from ever being issued.
  4. For CI, skip the interactive device flow entirely and set CAVE_TOKEN instead of polling for approval.
  5. If timeouts happen even when approving quickly, verify the machine clock is correct and check whether the server's expires_in is suspiciously small.

Example fix

// before: start the flow, get distracted, code expires
const result = await runCavemanDeviceFlow(options);

// after: surface the code early and finish approval promptly, or fall back to a token
try {
  const result = await runCavemanDeviceFlow({
    ...options,
    onCode: (code) => {
      console.log(`Open ${code.verification_uri} and enter ${code.user_code} (expires in ${code.expires_in}s)`);
    },
  });
} catch (error) {
  if (error instanceof Error && error.message === "device login timed out before approval") {
    console.error("Approval not completed in time; run `caveman login` again or set CAVE_TOKEN.");
    process.exitCode = 1;
  } else throw error;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before starting the device flow, confirm a human can act now.
function canRunDeviceFlow(): boolean {
  const interactive = Boolean(process.stdout.isTTY) && !process.env.CI;
  const hasStaticToken = Boolean(process.env.CAVE_TOKEN);
  return interactive || hasStaticToken;
}
if (!canRunDeviceFlow()) {
  throw new Error("Device flow needs a TTY for approval or CAVE_TOKEN for CI.");
}

Type guard

function isDeviceTimeout(error: unknown): error is Error {
  return error instanceof Error &&
    error.message === "device login timed out before approval";
}

Try / catch

try {
  const { code, credentials } = await runCavemanDeviceFlow({
    onCode: (c) => console.log(`Open ${c.verification_uri} and enter ${c.user_code} (expires in ${c.expires_in}s)`),
  });
  await credentials.acknowledge();
} catch (error) {
  if (isDeviceTimeout(error)) {
    console.error("Approval window expired — rerun `caveman login` and approve promptly, or set CAVE_TOKEN.");
    process.exitCode = 1;
  } else {
    throw error;
  }
}

Prevention

When it happens

Trigger: The poll loop at packages/device-auth/src/index.ts:113 runs until `Date.now() < deadline`; every poll returns either authorization_pending or slow_down (loop keeps waiting) or an access_token (function returns). The throw fires only when the loop condition becomes false: expires_in seconds elapsed with no access_token. Transient poll fetch errors are retried, not fatal, unless the deadline also passed (then a different 'device login polling failed' error is thrown).

Common situations: Running `caveman login` (or any flow calling runCavemanDeviceFlow) and walking away without opening verification_uri and entering user_code; opening the verification URL after the code expired (typically 5–15 minutes); the approval page being open but the org/admin approval never clicked; clock skew or a very short expires_in from the server making the window effectively zero; the user approving in a different account/tenant than the device_code belongs to.

Understand the failure class

Related errors


AI-assisted analysis of JuliusBrussee/caveman@df2ccd85c9 (2026-08-31). Data as JSON: /api/errors/65b514f0175bf1af. Report an issue: GitHub.