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
- Re-run the login command to get a fresh device code, then open the verification_uri and enter the user_code promptly.
- Make sure onCode surfaced the code visibly (open the verification_uri in a browser and enter user_code before expires_in elapses).
- Check server-side approval requirements (admin consent, correct org) that might silently block the token from ever being issued.
- For CI, skip the interactive device flow entirely and set CAVE_TOKEN instead of polling for approval.
- 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
- Always pass onCode and open/print verification_uri immediately; treat expires_in as a hard deadline.
- Use CAVE_TOKEN in CI and headless environments instead of the interactive device flow.
- Keep machine clocks synced (NTP) so expires_in bookkeeping is accurate.
- Never walk away mid-login; if approval may be delayed, restart the flow to get a fresh code rather than waiting on an expired one.
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
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- caveman-code: no supported provider credential found; set AN
- caveman agent: tool timeoutMs must be a positive integer
- caveman agent: Cave Runtime did not become ready at ${gatewa
- timeoutMs must be a positive integer
- binary download failed: ${error.message}
AI-assisted analysis of JuliusBrussee/caveman@df2ccd85c9 (2026-08-31).
Data as JSON: /api/errors/65b514f0175bf1af.
Report an issue: GitHub.