zeroclaw-labs/zeroclaw · error · anyhow::Error
GitHub device authorization expired
Error message
GitHub device authorization expired
What it means
GitHub's OAuth device flow returns expired_token when the user_code shown at github.com/login/device was not confirmed within expires_in (typically about 15 minutes). The poll loop in device_code_login aborts instead of continuing to poll a dead code, so login must be restarted.
Source
Thrown at crates/zeroclaw-providers/src/copilot.rs:600
"grant_type": "urn:ietf:params:oauth:grant-type:device_code"
}))
.send()
.await?
.json()
.await?;
if let Some(token) = token_response.access_token {
eprintln!("Authentication succeeded.\n");
return Ok(token);
}
match token_response.error.as_deref() {
Some("slow_down") => {
poll_interval += Duration::from_secs(5);
}
Some("authorization_pending") | None => {}
Some("expired_token") => {
anyhow::bail!("GitHub device authorization expired")
}
Some(error) => anyhow::bail!("GitHub auth failed: {error}"),
}
}
anyhow::bail!("Timed out waiting for GitHub authorization")
}
/// Exchange a GitHub access token for a Copilot API key.
async fn exchange_for_api_key(&self, access_token: &str) -> anyhow::Result<ApiKeyInfo> {
let mut request = self.http_client().get(GITHUB_API_KEY_URL);
for (header, value) in &Self::COPILOT_HEADERS {
request = request.header(*header, *value);
}
request = request.header("Authorization", format!("token {access_token}"));
let response = request.send().await?;
View on GitHub (pinned to 88bb9c8533)
Solutions
- Re-run the Copilot login and complete the browser prompt within the window (~15 minutes)
- Copy the user_code exactly from the terminal when prompted at github.com/login/device
- For headless machines, authenticate once elsewhere and copy the token dir, or configure a GitHub token in config instead of device flow
- Keep the terminal session alive - killing it discards the pending device code
Defensive patterns
Strategy: retry
Try / catch
const MAX_ATTEMPTS: usize = 3;
for attempt in 1..=MAX_ATTEMPTS {
match copilot_login().await {
Ok(token) => break_ok(token),
Err(e) if e.to_string().contains("device authorization expired") => {
if attempt == MAX_ATTEMPTS { return Err(e); }
continue; // fresh device code, user gets a new window
}
Err(e) => return Err(e),
}
} Prevention
- Show the user_code and URL prominently and complete the browser step immediately
- Run interactive logins on machines with a browser reachable by the user
- For headless boxes, authenticate once interactively and copy the token dir
- Do not leave device-flow sessions unattended past the ~15 minute window
When it happens
Trigger: device_code_login polls GITHUB_ACCESS_TOKEN_URL; the response error field is expired_token because nobody entered and approved the code in the browser before the deadline. Only raised during first-time Copilot login (cached tokens skip the flow).
Common situations: Unattended or headless login left waiting; user steps away from the browser; slow relaying of the code to the user; reusing a stale login attempt.
Related errors
- GitHub auth failed: {error}
- Timed out waiting for GitHub authorization
- Failed to get Copilot API key ({status}): {sanitized}. Ensur
- Git channel requires the `channel-git` feature
- Google device code request failed ({}): {}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/307cc83bffb94971.
Report an issue: GitHub.