Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-keyring-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": minor
---

Add `GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND` env var for explicit keyring backend selection (`keyring` or `file`). Fixes credential key loss in Docker/keyring-less environments by never deleting `.encryption_key` and always persisting it as a fallback.
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ Use these labels to categorize pull requests and issues:
| Variable | Description |
|---|---|
| `GOOGLE_WORKSPACE_CLI_TOKEN` | Pre-obtained OAuth2 access token (highest priority; bypasses all credential file loading) |
| `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` | Path to OAuth credentials JSON (no default; if unset, falls back to credentials secured by the OS Keyring and encrypted in `~/.config/gws/`) |
| `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` | Path to OAuth credentials JSON (no default; if unset, falls back to encrypted credentials in `~/.config/gws/`) |
| `GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND` | Keyring backend: `keyring` (default, uses OS keyring with file fallback) or `file` (file only, for Docker/CI/headless) |

| `GOOGLE_APPLICATION_CREDENTIALS` | Standard Google ADC path; used as fallback when no gws-specific credentials are configured |

Expand Down
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ keyring = "3.6.3"
async-trait = "0.1.89"
serde_yaml = "0.9.34"
percent-encoding = "2.3.2"
zeroize = { version = "1.8.2", features = ["derive"] }


# The profile that 'cargo dist' will build with
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ The CLI supports multiple auth workflows so it works on your laptop, in CI, and

### Interactive (local desktop)

Credentials are encrypted at rest (AES-256-GCM) with the key stored in your OS keyring.
Credentials are encrypted at rest (AES-256-GCM) with the key stored in your OS keyring (or `~/.config/gws/.encryption_key` when `GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file`).

```bash
gws auth setup # one-time: creates a Cloud project, enables APIs, logs you in
Expand Down
1 change: 1 addition & 0 deletions skills/gws-gmail-forward/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ gws gmail +forward --message-id 18f1a2b3c4d --to dave@example.com --cc eve@examp
## Tips

- Includes the original message with sender, date, subject, and recipients.
- Sends the forward as a new message rather than forcing it into the original thread.

## See Also

Expand Down
3 changes: 2 additions & 1 deletion src/auth_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> {
"message": "Authentication successful. Encrypted credentials saved.",
"account": actual_email.as_deref().unwrap_or("(unknown)"),
"credentials_file": enc_path.display().to_string(),
"encryption": "AES-256-GCM (key secured by OS Keyring or local `.encryption_key`)",
"encryption": "AES-256-GCM (key in OS keyring or local `.encryption_key`; set GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file for headless)",
"scopes": scopes,
});
println!(
Expand Down Expand Up @@ -944,6 +944,7 @@ async fn handle_status() -> Result<(), GwsError> {
let mut output = json!({
"auth_method": auth_method,
"storage": storage,
"keyring_backend": credential_store::active_backend_name(),
"encrypted_credentials": enc_path.display().to_string(),
"encrypted_credentials_exists": has_encrypted,
"plain_credentials": plain_path.display().to_string(),
Expand Down
Loading
Loading