gws is a Rust CLI tool for interacting with Google Workspace APIs. It dynamically generates its command surface at runtime by parsing Google Discovery Service JSON documents.
Important
Dynamic Discovery: This project does NOT use generated Rust crates (e.g., google-drive3) for API interaction. Instead, it fetches the Discovery JSON at runtime and builds clap commands dynamically. When adding a new service, you only need to register it in src/services.rs and verify the Discovery URL pattern in src/discovery.rs. Do NOT add new crates to Cargo.toml for standard Google APIs.
Note
Package Manager: Use pnpm instead of npm for Node.js package management in this repository.
Important
Test Coverage: The codecov/patch check requires that new or modified lines are covered by tests. When adding code, extract testable helper functions rather than embedding logic in main/run where it's hard to unit-test. Run cargo test locally and verify new branches are exercised.
cargo build # Build in dev mode
cargo clippy -- -D warnings # Lint check
cargo test # Run testsEvery PR must include a changeset file. Create one at .changeset/<descriptive-name>.md:
---
"@googleworkspace/cli": patch
---
Brief description of the changeUse patch for fixes/chores, minor for new features, major for breaking changes. The CI policy check will fail without a changeset.
The CLI uses a two-phase argument parsing strategy:
- Parse argv to extract the service name (e.g.,
drive) - Fetch the service's Discovery Document, build a dynamic
clap::Commandtree, then re-parse
| File | Purpose |
|---|---|
src/main.rs |
Entrypoint, two-phase CLI parsing, method resolution |
src/discovery.rs |
Serde models for Discovery Document + fetch/cache |
src/services.rs |
Service alias → Discovery API name/version mapping |
src/auth.rs |
OAuth2 token acquisition via env vars, encrypted credentials, or ADC |
src/credential_store.rs |
AES-256-GCM encryption/decryption of credential files |
src/auth_commands.rs |
gws auth subcommands: login, logout, setup, status, export |
src/commands.rs |
Recursive clap::Command builder from Discovery resources |
src/executor.rs |
HTTP request construction, response handling, schema validation |
src/schema.rs |
gws schema command — introspect API method schemas |
src/error.rs |
Structured JSON error output |
src/logging.rs |
Opt-in structured logging (stderr + file) via tracing |
src/timezone.rs |
Account timezone resolution: --timezone flag, Calendar Settings API, 24h cache |
Demo recordings are generated with VHS (.tape files).
vhs docs/demo.tape- Use double quotes for simple strings:
Type "gws --help" Enter - Use backtick quotes when the typed text contains JSON with double quotes:
Type `gws drive files list --params '{"pageSize":5}'` Enter\"escapes inside double-quotedTypestrings are not supported by VHS and will cause parse errors.
ASCII art title cards live in art/. The scripts/show-art.sh helper clears the screen and cats the file. Portrait scenes use scene*.txt; landscape chapters use long-*.txt.
Important
This CLI is frequently invoked by AI/LLM agents. Always assume inputs can be adversarial — validate paths against traversal (../../.ssh), restrict format strings to allowlists, reject control characters, and encode user values before embedding them in URLs.
Note
Environment variables are trusted inputs. The validation rules above apply to CLI arguments that may be passed by untrusted AI agents. Environment variables (e.g. GOOGLE_WORKSPACE_CLI_CONFIG_DIR) are set by the user themselves — in their shell profile, .env file, or deployment config — and are not subject to path traversal validation. This is consistent with standard conventions like XDG_CONFIG_HOME, CARGO_HOME, etc.
When adding new helpers or CLI flags that accept file paths, always validate using the shared helpers:
| Scenario | Validator | Rejects |
|---|---|---|
File path for writing (--output-dir) |
validate::validate_safe_output_dir() |
Absolute paths, ../ traversal, symlinks outside CWD, control chars |
File path for reading (--dir) |
validate::validate_safe_dir_path() |
Absolute paths, ../ traversal, symlinks outside CWD, control chars |
Enum/allowlist values (--msg-format) |
clap value_parser (see gmail/mod.rs) |
Any value not in the allowlist |
// In your argument parser:
if let Some(output_dir) = matches.get_one::<String>("output-dir") {
crate::validate::validate_safe_output_dir(output_dir)?;
builder.output_dir(Some(output_dir.clone()));
}User-supplied values embedded in URL path segments must be percent-encoded. Use the shared helper:
// CORRECT — encodes slashes, spaces, and special characters
let url = format!(
"https://www.googleapis.com/drive/v3/files/{}",
crate::helpers::encode_path_segment(file_id),
);
// WRONG — raw user input in URL path
let url = format!("https://www.googleapis.com/drive/v3/files/{}", file_id);For query parameters, use reqwest's .query() builder which handles encoding automatically:
// CORRECT — reqwest encodes query values
client.get(url).query(&[("q", user_query)]).send().await?;
// WRONG — manual string interpolation in query strings
let url = format!("{}?q={}", base_url, user_query);When a user-supplied string is used as a GCP resource identifier (project ID, topic name, space name, etc.) that gets embedded in a URL path, validate it first:
// Validates the string does not contain path traversal segments (`..`), control characters, or URL-breaking characters like `?` and `#`.
let project = crate::helpers::validate_resource_name(&project_id)?;
let url = format!("https://pubsub.googleapis.com/v1/projects/{}/topics/my-topic", project);This prevents injection of query parameters, path traversal, or other malicious payloads through resource name arguments like --project or --space.
When adding a new helper or CLI command:
- File paths → Use
validate_safe_output_dir/validate_safe_dir_path - Enum flags → Constrain via clap
value_parserorvalidate_msg_format - URL path segments → Use
encode_path_segment() - Query parameters → Use reqwest
.query()builder - Resource names (project IDs, space names, topic names) → Use
validate_resource_name() - Write tests for both the happy path AND the rejection path (e.g., pass
../../.sshand assertErr)
Use these labels to categorize pull requests and issues:
area: discovery— Discovery document fetching, caching, parsingarea: http— Request execution, URL building, response handlingarea: docs— README, contributing guides, documentationarea: tui— Setup wizard, picker, input fieldsarea: distribution— Nix flake, cargo-dist, npm packaging, install methodsarea: auth— OAuth, credentials, multi-account, ADCarea: skills— AI skill generation and management
| 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 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 |
| Variable | Description |
|---|---|
GOOGLE_WORKSPACE_CLI_CONFIG_DIR |
Override the config directory (default: ~/.config/gws) |
| Variable | Description |
|---|---|
GOOGLE_WORKSPACE_CLI_CLIENT_ID |
OAuth client ID (for gws auth login when no client_secret.json is saved) |
GOOGLE_WORKSPACE_CLI_CLIENT_SECRET |
OAuth client secret (paired with CLIENT_ID above) |
| Variable | Description |
|---|---|
GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE |
Default Model Armor template (overridden by --sanitize flag) |
GOOGLE_WORKSPACE_CLI_SANITIZE_MODE |
warn (default) or block |
| Variable | Description |
|---|---|
GOOGLE_WORKSPACE_PROJECT_ID |
GCP project ID override for quota/billing and fallback for helper commands (overridden by --project flag) |
| Variable | Description |
|---|---|
GOOGLE_WORKSPACE_CLI_LOG |
Log level filter for stderr output (e.g., gws=debug). Off by default. |
GOOGLE_WORKSPACE_CLI_LOG_FILE |
Directory for JSON-line log files with daily rotation. Off by default. |
All variables can also live in a .env file (loaded via dotenvy).