xai-org/grok-build · error

Failed to save credentials

Error message

Failed to save credentials: {e}

What it means

build_auth finishes device-code login by persisting the enriched Auth via auth_manager.update; a persistence failure is wrapped as 'Failed to save credentials'.

Solutions

  1. Check the underlying update error in the message chain for the concrete cause (permissions, keyring, disk)
  2. Verify the credentials file/directory is writable (or the OS keyring is reachable in headless environments)
  3. Set an explicit credentials-file path the process can write if the default location is restricted
  4. Ensure no concurrent login processes are racing on the same store

Example fix

// before
XDG_DATA_HOME unset -> keyring missing in headless CI
// after
export XDG_DATA_HOME=/tmp/xdg  # writable file-backed credential store
<run device-code login again>
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check storage writability
let dir = credentials_dir()?;
assert!(dir.exists() && !dir.permissions().readonly(), "credential store not writable");

Try / catch

match build_auth(...).await {
    Err(e) if e.to_string().contains("Failed to save credentials") => {
        eprintln!("persisting auth failed: {e}; check keyring/permissions");
        // fall back to a file-based credentials path
    }
    other => other?,
}

Prevention

When it happens

Trigger: auth_manager.update returning Err — credential store unwritable, keychain/keyring unavailable, locked or corrupt credentials file, or storage backend rejecting the update.

Common situations: Read-only home dir or disk full in CI containers, headless Linux without a keyring service, file permissions denying access to the credentials path, concurrent login runs corrupting the store.

Related errors


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/d9dcf3e3be363078. Report an issue: GitHub.

Appendix: source

Thrown at crates/codegen/xai-grok-shell/src/auth/device_code.rs:492

        team_id,
        team_name: None,
        team_role: None,
        user_blocked_reason: None,
        team_blocked_reasons: vec![],
        coding_data_retention_opt_out: crate::auth::default_coding_data_retention_opt_out(),
        has_grok_code_access: None,
        refresh_token: tokens.refresh_token.clone(),
        expires_at: tokens.expires_in.map(|s| now + Duration::seconds(s)),
        oidc_issuer: Some(issuer.to_owned()),
        oidc_client_id: Some(client_id.to_owned()),
    };

    auth_manager.enrich_auth_inline(&mut auth).await;

    auth_manager
        .update(auth)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to save credentials: {e}"))
}

/// Decode JWT payload without signature verification.
/// Returns (sub, Option<email>).
fn decode_jwt_claims(jwt: &str) -> (String, Option<String>) {
    use base64::Engine;
    let parts: Vec<&str> = jwt.splitn(3, '.').collect();
    if parts.len() < 2 {
        return (String::new(), None);
    }
    let payload = match base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(parts[1]) {
        Ok(bytes) => bytes,
        Err(_) => return (String::new(), None),
    };
    let claims: IdTokenClaims = match serde_json::from_slice(&payload) {
        Ok(claims) => claims,
        Err(_) => return (String::new(), None),
    };

View on GitHub (pinned to bc7f02eddd)