{"record":{"id":"9d01d7053102770e","repo":"googleworkspace/cli","slug":"failed-to-write-credentials-e","errorCode":null,"errorMessage":"Failed to write credentials: {e}","messagePattern":"Failed to write credentials: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/google-workspace-cli/src/credential_store.rs","lineNumber":453,"sourceCode":"        #[cfg(unix)]\n        {\n            use std::os::unix::fs::PermissionsExt;\n            if let Err(e) = std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))\n            {\n                eprintln!(\n                    \"Warning: failed to set directory permissions on {}: {e}\",\n                    parent.display()\n                );\n            }\n        }\n    }\n\n    let encrypted = encrypt(json.as_bytes())?;\n\n    // Write atomically via a sibling .tmp file + rename so the credentials\n    // file is never left in a corrupt partial-write state on crash/Ctrl-C.\n    crate::fs_util::atomic_write(&path, &encrypted)\n        .map_err(|e| anyhow::anyhow!(\"Failed to write credentials: {e}\"))?;\n\n    Ok(path)\n}\n\n/// Loads and decrypts credentials JSON from a specific path.\npub fn load_encrypted_from_path(path: &std::path::Path) -> anyhow::Result<String> {\n    let data = std::fs::read(path)?;\n    let plaintext = decrypt(&data)?;\n    Ok(String::from_utf8(plaintext)?)\n}\n\n/// Loads and decrypts credentials JSON from the default encrypted file.\npub fn load_encrypted() -> anyhow::Result<String> {\n    load_encrypted_from_path(&encrypted_credentials_path())\n}\n\n#[cfg(test)]\nmod tests {","sourceCodeStart":435,"sourceCodeEnd":471,"githubUrl":"https://github.com/googleworkspace/cli/blob/a3768d0e82ad83cca2da97724e46bea4ff0e6dbd/crates/google-workspace-cli/src/credential_store.rs#L435-L471","documentation":"The final persistence step in save: after encrypting the JSON, the bytes are written via atomic_write (sibling .tmp + rename). This error wraps any failure of that write — permission denied on the config dir, read-only filesystem, disk full, or the target path being unusable. The atomic strategy means the previous credentials file is never left half-written by these failures.","triggerScenarios":"~/.config/gws owned by root after running gws with sudo; GOOGLE_WORKSPACE_CLI_CONFIG_DIR pointing at a read-only mount; disk quota exhausted; directory in the path replaced by a file; SELinux denying writes.","commonSituations":"Ran `sudo gws auth login` once, then normal user cannot write; container with a read-only config mount; CI runner with a full disk; config dir on a hardened NFS mount.","solutions":["Check ownership: `ls -ld ~/.config/gws` and fix with chown if it is owned by root","Verify writability: `touch ~/.config/gws/.probe` (or your GOOGLE_WORKSPACE_CLI_CONFIG_DIR) and check disk space with `df -h`","Point GOOGLE_WORKSPACE_CLI_CONFIG_DIR at a writable directory, or fix the mount options / SELinux policy"],"exampleFix":"# before\n$ sudo gws auth login   # creates root-owned ~/.config/gws\n$ gws auth login         # Failed to write credentials\n\n# after\n$ sudo chown -R \"$USER:$USER\" ~/.config/gws\n$ gws auth login","handlingStrategy":"validation","validationCode":"// Preflight: prove the config dir is writable before login\nuse std::io::Write;\n\nfn config_dir_writable(dir: &std::path::Path) -> bool {\n    std::fs::create_dir_all(dir).is_ok() && {\n        let probe = dir.join(\".write_probe\");\n        std::fs::File::create(&probe).and_then(|mut f| f.write_all(b\"x\")).is_ok()\n            && std::fs::remove_file(&probe).is_ok()\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never run gws with sudo as a habit — it leaves root-owned files in ~/.config/gws","Check `df -h` and `ls -ld` on the config dir as part of provisioning scripts","Mount a writable volume at GOOGLE_WORKSPACE_CLI_CONFIG_DIR in containers"],"tags":["file-io","permissions","credentials","disk-full"],"backgroundTag":"file-write-permission-denied","analyzedSha":"a3768d0e82ad83cca2da97724e46bea4ff0e6dbd","analyzedAt":"2026-08-16T19:51:46.516Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}