googleworkspace/cli · error · anyhow::Error

Invalid client_secret.json format: {e}

Error message

Invalid client_secret.json format: {e}

What it means

The client_secret.json file was read but `serde_json::from_str::<ClientSecretFile>` failed. The struct requires a top-level `installed` key (see the format documented at the top of oauth_config.rs); the serde error text says exactly which field broke. The classic cause is downloading the wrong OAuth client type from Google Cloud Console — a Web application client downloads `{"web": {...}}`, which deserializes to nothing here.

Source

Thrown at crates/google-workspace-cli/src/oauth_config.rs:96

    let path = client_config_path();
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }

    let json = serde_json::to_string_pretty(&config)?;
    crate::fs_util::atomic_write(&path, json.as_bytes())
        .map_err(|e| anyhow::anyhow!("Failed to write client config: {e}"))?;

    Ok(path)
}

/// Loads OAuth client configuration from the standard Google Cloud Console format.
pub fn load_client_config() -> anyhow::Result<InstalledConfig> {
    let path = client_config_path();
    let data = std::fs::read_to_string(&path)
        .map_err(|e| anyhow::anyhow!("Cannot read {}: {e}", path.display()))?;
    let file: ClientSecretFile = serde_json::from_str(&data)
        .map_err(|e| anyhow::anyhow!("Invalid client_secret.json format: {e}"))?;
    Ok(file.installed)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_save_load_round_trip() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("client_secret.json");

        let config = ClientSecretFile {
            installed: InstalledConfig {
                client_id: "test-id.apps.googleusercontent.com".to_string(),
                client_secret: "GOCSPX-test".to_string(),
                project_id: "my-project".to_string(),
                auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(),

View on GitHub (pinned to a3768d0e82)

Solutions

  1. In Google Cloud Console → APIs & Services → Credentials, create an OAuth client of type **Desktop app** and download that JSON — it contains the `installed` key.
  2. If you must keep the Web client, its JSON has a `web` key — either re-wrap it as `installed` (copying client_id/client_secret/auth_uri/token_uri) or use env vars instead.
  3. Validate the file locally: `jq 'keys' client_secret.json` must print ["installed"].
  4. If you meant to use a credentials file rather than a client secret, set `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` instead of placing it at client_secret.json.

Example fix

# before — Web client JSON fails to deserialize
jq 'keys' ~/.config/gws/client_secret.json   # ["web"]
gws auth login  # -> Invalid client_secret.json format: missing field `installed`

# after — download a Desktop app client, or rewrite the envelope
jq '{installed: .web | {client_id, client_secret, project_id,
     auth_uri: "https://accounts.google.com/o/oauth2/auth",
     token_uri: "https://oauth2.googleapis.com/token"}}' \
  web_secret.json > ~/.config/gws/client_secret.json
gws auth login
Defensive patterns

Strategy: validation

Validate before calling

// Validate the secret file shape before handing it to gws
use serde_json::Value;
fn is_desktop_client_secret(path: &std::path::Path) -> anyhow::Result<bool> {
    let v: Value = serde_json::from_str(&std::fs::read_to_string(path)?)?;
    Ok(v.get("installed").is_some())
}

Try / catch

match serde_json::from_str::<ClientSecretFile>(&data) {
    Err(e) if data.trim_start().starts_with('{') && data.contains("\"web\"") => {
        eprintln!("this is a Web-application client — download a Desktop app client instead");
        Err(anyhow::anyhow!("Invalid client_secret.json format: {e}"))
    }
    other => other.map(|f: ClientSecretFile| f.installed),
}

Prevention

When it happens

Trigger: User saved a 'Web application' or 'Chrome app' client JSON instead of 'Desktop app'; the file was truncated or empty; the file is actually the OAuth *token/credentials* JSON (client env credentials, service-account key) rather than the client secret; hand-edited JSON with a syntax error or a renamed key.

Common situations: Following generic Google OAuth docs that default to Web clients; copy-pasting only part of the JSON; confusing the service-account key file with the client secret; saving with a BOM or wrapped in quotes.

Related errors


AI-assisted analysis of googleworkspace/cli@a3768d0e82 (2026-08-16). Data as JSON: /api/errors/454040c423131529. Report an issue: GitHub.