zeroclaw-labs/zeroclaw · error · anyhow::Error

Gemini CLI OAuth token expired and no refresh_token availabl

Error message

Gemini CLI OAuth token expired and no refresh_token available — re-run `gemini` to authenticate

What it means

The Gemini CLI OAuth credential state holds an access token that is expired (or within the 60-second pre-expiry refresh buffer, or of unknown expiry) and has no refresh_token, so get_valid_oauth_token cannot renew it in-process. The only recovery is re-authenticating through the Gemini CLI, as the message states.

Source

Thrown at crates/zeroclaw-providers/src/gemini.rs:834

            .is_none_or(|exp| exp <= now_millis.saturating_add(60_000));

        if needs_refresh {
            if let Some(ref refresh_token) = guard.refresh_token {
                let refreshed = refresh_gemini_cli_token_async(
                    refresh_token,
                    guard.client_id.as_deref(),
                    guard.client_secret.as_deref(),
                )
                .await?;
                ::zeroclaw_log::record!(
                    INFO,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
                    "Gemini CLI OAuth token refreshed successfully (runtime)"
                );
                guard.access_token = refreshed.access_token;
                guard.expiry_millis = refreshed.expiry_millis;
            } else {
                anyhow::bail!(
                    "Gemini CLI OAuth token expired and no refresh_token available — re-run `gemini` to authenticate"
                );
            }
        }

        Ok(guard.access_token.clone())
    }

    /// Rotate to the next available OAuth credentials file and swap state.
    /// Returns `true` when rotation succeeded.
    async fn rotate_oauth_credential(
        &self,
        state: &Arc<tokio::sync::Mutex<OAuthTokenState>>,
    ) -> bool {
        if self.oauth_cred_paths.len() <= 1 {
            return false;
        }

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Run `gemini` and log in again so a complete oauth_creds.json (with refresh_token) is written
  2. Delete the stale credential files under the Gemini CLI config dir and log in fresh
  3. Or configure a Gemini API key on the alias to bypass CLI OAuth entirely
  4. If this recurs after every restart, check that nothing truncates or rewrites the credential file
Defensive patterns

Strategy: fallback

Validate before calling

// Check credential completeness before building the provider
if gemini_cli_creds_present() && !gemini_cli_creds_have_refresh_token() {
    // incomplete credentials: fail fast with a re-login hint at startup
    anyhow::bail!("Gemini CLI creds lack refresh_token; re-run `gemini`");
}

Try / catch

match provider.chat(/* ... */).await {
    Ok(resp) => Ok(resp),
    Err(e) if e.to_string().contains("no refresh_token available") => {
        // terminal for this credential set: instruct `gemini` re-login,
        // or fall back to an API-key-based gemini alias
        fallback_to_api_key_alias().await
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: get_valid_oauth_token needs a refresh but guard.refresh_token is None: the loaded ~/.gemini credential file contains an access token without a refresh_token field - an old format, a partially-written file, or a token saved before full consent.

Common situations: Upgraded or uninstalled Gemini CLI leaving stale credentials; credential files copied or edited without the refresh_token field; headless setups that never completed the full CLI login.

Understand the failure class

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/3c2f16262e93229d. Report an issue: GitHub.