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
- In Google Cloud Console → APIs & Services → Credentials, create an OAuth client of type **Desktop app** and download that JSON — it contains the `installed` key.
- 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.
- Validate the file locally: `jq 'keys' client_secret.json` must print ["installed"].
- 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
- Always create OAuth clients of type 'Desktop app' for CLI use — the JSON must have an `installed` key.
- Automate checks: `jq -e 'has("installed")' client_secret.json` in provisioning pipelines.
- Never hand-edit the secret JSON; download it fresh from Cloud Console.
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
- Token refresh failed with status {}: {}
- Token response contained no access token
- Failed to write client config: {e}
- Cannot read {}: {e}
- No credentials found. Run `gws auth setup` to configure, `gw
AI-assisted analysis of googleworkspace/cli@a3768d0e82 (2026-08-16).
Data as JSON: /api/errors/454040c423131529.
Report an issue: GitHub.