xai-org/grok-build · error

Failed to save credentials: {e}

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'.

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)

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.