* feat(gmail): add +reply, +reply-all, and +forward helper commands Add first-class reply and forward support to the Gmail helpers, addressing the gap described in #88. These commands handle the complex RFC 2822 threading mechanics (In-Reply-To, References, threadId) that agents and CLI users struggle with today. New commands: - +reply: reply to a message with automatic threading - +reply-all: reply to all recipients with --remove/--cc support - +forward: forward a message with quoted original content * fix(gmail): encode message_id in URL path and fix auth signature - Use crate::validate::encode_path_segment() on message_id in fetch_message_metadata URL construction per AGENTS.md rules - Update auth::get_token calls to pass None for the new account parameter added on main * refactor(gmail): extract send_raw_email and deduplicate handlers - Add send_raw_email() to mod.rs: shared encode→json→auth→execute pattern for sending raw RFC 2822 messages via users.messages.send - Simplify handle_reply: delegate send logic to send_raw_email - Simplify handle_forward: delegate send logic to send_raw_email Addresses code duplication feedback from PR review. * fix(gmail): register --dry-run flag on reply/forward commands The handlers read matches.get_flag("dry-run") but the flag was missing from the clap command definitions, so it always returned false. Now dry-run works for +reply, +reply-all, and +forward. * chore: add changeset for gmail reply/forward feature * style: apply cargo fmt formatting * fix(gmail): register --dry-run flag on +send command Same class of bug fixed for +reply/+reply-all/+forward — the handler reads matches.get_flag("dry-run") but the arg was not registered. * fix(gmail): honor Reply-To header and use exact address matching - Prefer Reply-To over From when selecting reply recipients, fixing incorrect routing for mailing lists and support systems - Use exact email address comparison instead of substring matching for --remove filtering and sender deduplication, preventing unintended recipient removal (e.g. ann@ no longer drops joann@) * test(gmail): add comprehensive coverage for reply address handling - extract_email: malformed input (no closing bracket), empty string, whitespace-only - build_reply_all_recipients: display-name sender exclusion, --remove with display name, extra --cc, CC becomes None when all filtered, case-insensitive sender exclusion * Improves reply-all recipient deduplication Corrects how `build_reply_all_recipients` handles multi-address `Reply-To` headers. Previously, only the first address from `Reply-To` was used for deduplication, leading to potential redundancy by including those addresses in the `Cc` field. The updated logic now parses all addresses in `Reply-To`, ensuring they are fully moved to the `To` field and properly excluded from `Cc`. * style(gmail): add missing Apache 2.0 copyright headers reply.rs and forward.rs were missing the copyright header that all other source files in the repo include. * fix(gmail): use try_get_one for optional --remove arg in +reply parse_reply_args used get_one("remove") which panics when called from +reply (which does not register --remove). Switch to try_get_one to safely return None for unregistered args. * feat(gmail): support --dry-run without auth for reply/forward commands Skip auth and message fetch when --dry-run is set by using placeholder OriginalMessage data. This lets users preview the request structure without needing credentials. * fix(gmail): use RFC-aware mailbox list parsing for recipient splitting Replace naive comma-split with split_mailbox_list that respects quoted strings, so display names containing commas like "Doe, John" <john@example.com> are handled correctly in reply-all recipient parsing, deduplication, and --remove filtering. * fix(gmail): handle escaped quotes in mailbox list splitting split_mailbox_list toggled quote state on every `"` without accounting for backslash-escaped quotes (`\"`), causing display names like `"Doe \"JD, Sr\""` to split incorrectly at interior commas. Track `prev_backslash` so `\"` inside quoted strings is treated as a literal quote character rather than a delimiter toggle. Double backslashes (`\\`) are handled correctly as well. * fix(gmail): address PR review feedback for reply/forward helpers - Use reqwest .query() for metadata params per AGENTS.md convention - Add MIME-Version and Content-Type headers to raw messages - Add --from flag to +reply, +reply-all, +forward for send-as/alias - Narrow ReplyConfig/ForwardConfig visibility to pub(super) - Refactor create_reply_raw_message args into ReplyEnvelope struct * fix(gmail): address review feedback for reply/forward helpers - Exclude authenticated user's own email from reply-all CC by fetching user profile via Gmail API - Use format=full to extract full plain-text body instead of truncated snippet for quoting and forwarding - Deduplicate CC addresses using a HashSet - Reuse auth token from message fetch in send_raw_email to eliminate double auth round-trip - Propagate auth errors in send_raw_email instead of silently falling back to unauthenticated requests - Use consistent CRLF line endings in quoted and forwarded message bodies per RFC 2822 * fix(gmail): Gmail reply and forward helpers * fix(gmail): refactor shared reply-forward helpers * Preserve repeated Gmail address headers * chore: regenerate skills [skip ci] --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
gws
One CLI for all of Google Workspace — built for humans and AI agents.
Drive, Gmail, Calendar, and every Workspace API. Zero boilerplate. Structured JSON output. 40+ agent skills included.
Important
Temporarily disabling non-collaborator pull requests. See this discussion, https://github.com/googleworkspace/cli/discussions/249.
Note
This is not an officially supported Google product.
npm install -g @googleworkspace/cli
gws doesn't ship a static list of commands. It reads Google's own Discovery Service at runtime and builds its entire command surface dynamically. When Google Workspace adds an API endpoint or method, gws picks it up automatically.
Important
This project is under active development. Expect breaking changes as we march toward v1.0.
Contents
- Prerequisites
- Installation
- Quick Start
- Why gws?
- Authentication
- AI Agent Skills
- Advanced Usage
- Environment Variables
- Architecture
- Troubleshooting
- Development
Prerequisites
- Node.js 18+ — for
npm install(or download a pre-built binary from GitHub Releases) - A Google Cloud project — required for OAuth credentials. You can create one via the Google Cloud Console or with the
gcloudCLI or with thegws auth setupcommand. - A Google account with access to Google Workspace
Installation
npm install -g @googleworkspace/cli
The npm package bundles pre-built native binaries for your OS and architecture. No Rust toolchain required.
Pre-built binaries are also available on the GitHub Releases page.
Or build from source:
cargo install --git https://github.com/googleworkspace/cli --locked
A Nix flake is also available at github:googleworkspace/cli
nix run github:googleworkspace/cli
Quick Start
gws auth setup # walks you through Google Cloud project config
gws auth login # subsequent OAuth login
gws drive files list --params '{"pageSize": 5}'
Why gws?
For humans — stop writing curl calls against REST docs. gws gives you --help on every resource, --dry-run to preview requests, and auto‑pagination.
For AI agents — every response is structured JSON. Pair it with the included agent skills and your LLM can manage Workspace without custom tooling.
# List the 10 most recent files
gws drive files list --params '{"pageSize": 10}'
# Create a spreadsheet
gws sheets spreadsheets create --json '{"properties": {"title": "Q1 Budget"}}'
# Send a Chat message
gws chat spaces messages create \
--params '{"parent": "spaces/xyz"}' \
--json '{"text": "Deploy complete."}' \
--dry-run
# Introspect any method's request/response schema
gws schema drive.files.list
# Stream paginated results as NDJSON
gws drive files list --params '{"pageSize": 100}' --page-all | jq -r '.files[].name'
Authentication
The CLI supports multiple auth workflows so it works on your laptop, in CI, and on a server.
Which setup should I use?
| I have… | Use |
|---|---|
gcloud installed and authenticated |
gws auth setup (fastest) |
A GCP project but no gcloud |
Manual OAuth setup |
| An existing OAuth access token | GOOGLE_WORKSPACE_CLI_TOKEN |
| Existing Credentials | GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE |
Interactive (local desktop)
Credentials are encrypted at rest (AES-256-GCM) with the key stored in your OS keyring.
gws auth setup # one-time: creates a Cloud project, enables APIs, logs you in
gws auth login # subsequent scope selection and login
gws auth setuprequires thegcloudCLI. If you don't havegcloud, use the manual setup below instead.
Warning
Scope limits in testing mode: If your OAuth app is unverified (testing mode), Google limits consent to ~25 scopes. The
recommendedscope preset includes 85+ scopes and will fail for unverified apps (especially for@gmail.comaccounts). Choose individual services instead to filter the scope picker:gws auth login -s drive,gmail,sheets
Manual OAuth setup (Google Cloud Console)
Use this when gws auth setup cannot automate project/client creation, or when you want explicit control.
- Open Google Cloud Console in the target project:
- OAuth consent screen:
https://console.cloud.google.com/apis/credentials/consent?project=<PROJECT_ID> - Credentials:
https://console.cloud.google.com/apis/credentials?project=<PROJECT_ID>
- OAuth consent screen:
- Configure OAuth branding/audience if prompted:
- App type: External (testing mode is fine)
- Add your account under Test users
- Create an OAuth client:
- Type: Desktop app
- Download the client JSON and save it to:
~/.config/gws/client_secret.json
Important
You must add yourself as a test user. In the OAuth consent screen, click Test users → Add users and enter your Google account email. Without this, login will fail with a generic "Access blocked" error.
Then run:
gws auth login
Browser-assisted auth (human or agent)
You can complete OAuth either manually or with browser automation.
- Human flow: run
gws auth login, open the printed URL, approve scopes. - Agent-assisted flow: the agent opens the URL, selects account, handles consent prompts, and returns control once the localhost callback succeeds.
If consent shows "Google hasn't verified this app" (testing mode), click Continue. If scope checkboxes appear, select required scopes (or Select all) before continuing.
Headless / CI (export flow)
- Complete interactive auth on a machine with a browser.
- Export credentials:
gws auth export --unmasked > credentials.json - On the headless machine:
export GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE=/path/to/credentials.json gws drive files list # just works
Service Account (server-to-server)
Point to your key file; no login needed.
export GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE=/path/to/service-account.json
gws drive files list
Pre-obtained Access Token
Useful when another tool (e.g. gcloud) already mints tokens for your environment.
export GOOGLE_WORKSPACE_CLI_TOKEN=$(gcloud auth print-access-token)
Precedence
| Priority | Source | Set via |
|---|---|---|
| 1 | Access token | GOOGLE_WORKSPACE_CLI_TOKEN |
| 2 | Credentials file | GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE |
| 3 | Encrypted credentials | gws auth login |
| 4 | Plaintext credentials | ~/.config/gws/credentials.json |
Environment variables can also live in a .env file.
AI Agent Skills
The repo ships 100+ Agent Skills (SKILL.md files) — one for every supported API, plus higher-level helpers for common workflows and 50 curated recipes for Gmail, Drive, Docs, Calendar, and Sheets. See the full Skills Index for the complete list.
# Install all skills at once
npx skills add https://github.com/googleworkspace/cli
# Or pick only what you need
npx skills add https://github.com/googleworkspace/cli/tree/main/skills/gws-drive
npx skills add https://github.com/googleworkspace/cli/tree/main/skills/gws-gmail
OpenClaw setup
# Symlink all skills (stays in sync with repo)
ln -s $(pwd)/skills/gws-* ~/.openclaw/skills/
# Or copy specific skills
cp -r skills/gws-drive skills/gws-gmail ~/.openclaw/skills/
The gws-shared skill includes an install block so OpenClaw auto-installs the CLI via npm if gws isn't on PATH.
Gemini CLI Extension
-
Authenticate the CLI first:
gws auth setup -
Install the extension into the Gemini CLI:
gemini extensions install https://github.com/googleworkspace/cli
Installing this extension gives your Gemini CLI agent direct access to all gws commands and Google Workspace agent skills. Because gws handles its own authentication securely, you simply need to authenticate your terminal once prior to using the agent, and the extension will automatically inherit your credentials.
Advanced Usage
Multipart Uploads
gws drive files create --json '{"name": "report.pdf"}' --upload ./report.pdf
Pagination
| Flag | Description | Default |
|---|---|---|
--page-all |
Auto-paginate, one JSON line per page (NDJSON) | off |
--page-limit <N> |
Max pages to fetch | 10 |
--page-delay <MS> |
Delay between pages | 100 ms |
Google Sheets — Shell Escaping
Sheets ranges use ! which bash interprets as history expansion. Always wrap values in single quotes:
# Read cells A1:C10 from "Sheet1"
gws sheets spreadsheets values get \
--params '{"spreadsheetId": "SPREADSHEET_ID", "range": "Sheet1!A1:C10"}'
# Append rows
gws sheets spreadsheets values append \
--params '{"spreadsheetId": "ID", "range": "Sheet1!A1", "valueInputOption": "USER_ENTERED"}' \
--json '{"values": [["Name", "Score"], ["Alice", 95]]}'
Model Armor (Response Sanitization)
Integrate Google Cloud Model Armor to scan API responses for prompt injection before they reach your agent.
gws gmail users messages get --params '...' \
--sanitize "projects/P/locations/L/templates/T"
| Variable | Description |
|---|---|
GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE |
Default Model Armor template |
GOOGLE_WORKSPACE_CLI_SANITIZE_MODE |
warn (default) or block |
Environment Variables
All variables are optional. See .env.example for a copy-paste template.
| Variable | Description |
|---|---|
GOOGLE_WORKSPACE_CLI_TOKEN |
Pre-obtained OAuth2 access token (highest priority) |
GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE |
Path to OAuth credentials JSON (user or service account) |
| GOOGLE_WORKSPACE_CLI_CLIENT_ID | OAuth client ID (alternative to client_secret.json) |
| GOOGLE_WORKSPACE_CLI_CLIENT_SECRET | OAuth client secret (paired with CLIENT_ID) |
| GOOGLE_WORKSPACE_CLI_CONFIG_DIR | Override config directory (default: ~/.config/gws) |
| GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE | Default Model Armor template |
| GOOGLE_WORKSPACE_CLI_SANITIZE_MODE | warn (default) or block |
| GOOGLE_WORKSPACE_PROJECT_ID | GCP project ID override for quota/billing and fallback for helper commands |
Environment variables can also be set in a .env file (loaded via dotenvy).
Architecture
gws uses a two-phase parsing strategy:
- Read
argv[1]to identify the service (e.g.drive) - Fetch the service's Discovery Document (cached 24 h)
- Build a
clap::Commandtree from the document's resources and methods - Re-parse the remaining arguments
- Authenticate, build the HTTP request, execute
All output — success, errors, download metadata — is structured JSON.
Troubleshooting
"Access blocked" or 403 during login
Your OAuth app is in testing mode and your account is not listed as a test user.
Fix: Open the OAuth consent screen in your GCP project → Test users → Add users → enter your Google account email. Then retry gws auth login.
"Google hasn't verified this app"
Expected when your app is in testing mode. Click Advanced → Go to <app name> (unsafe) to proceed. This is safe for personal use; verification is only required to publish the app to other users.
Too many scopes / consent screen error
Unverified (testing mode) apps are limited to ~25 OAuth scopes. The recommended scope preset includes many scopes and will exceed this limit.
Fix: Select only the scopes you need:
gws auth login --scopes drive,gmail,calendar
gcloud CLI not found
gws auth setup requires the gcloud CLI to automate project creation. You have three options:
- Install gcloud and use
gclouddirectly. - Re-run
gws auth setupwhich wrapsgcloudcalls. - Skip
gcloudentirely — set up OAuth credentials manually in the Cloud Console
redirect_uri_mismatch
The OAuth client was not created as a Desktop app type. In the Credentials page, delete the existing client, create a new one with type Desktop app, and download the new JSON.
API not enabled — accessNotConfigured
If a required Google API is not enabled for your GCP project, you will see a
403 error with reason accessNotConfigured:
{
"error": {
"code": 403,
"message": "Gmail API has not been used in project 549352339482 ...",
"reason": "accessNotConfigured",
"enable_url": "https://console.developers.google.com/apis/api/gmail.googleapis.com/overview?project=549352339482"
}
}
gws also prints an actionable hint to stderr:
💡 API not enabled for your GCP project.
Enable it at: https://console.developers.google.com/apis/api/gmail.googleapis.com/overview?project=549352339482
After enabling, wait a few seconds and retry your command.
Steps to fix:
- Click the
enable_urllink (or copy it from theenable_urlJSON field). - In the GCP Console, click Enable.
- Wait ~10 seconds, then retry your
gwscommand.
Tip
You can also run
gws auth setupwhich walks you through enabling all required APIs for your project automatically.
Development
cargo build # dev build
cargo clippy -- -D warnings # lint
cargo test # unit tests
./scripts/coverage.sh # HTML coverage report → target/llvm-cov/html/
License
Apache-2.0
Disclaimer
Caution
This is not an officially supported Google product.