Profile-aware Google Workspace CLI for humans and agents.
gw is a small facade around googleworkspace/cli (gws).
It adds named profiles, requires an explicit profile for every operation, and gives each profile a fully isolated gws configuration directory.
gws owns Google. gw owns identity selection and isolation.
gws has broad, Discovery-generated Google Workspace coverage, but no reliable first-class multi-account support.
Native multi-account support was removed upstream (googleworkspace/cli#439), and pointing GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE at a different file is not sufficient isolation because cached authentication state can still belong to another account (googleworkspace/cli#572).
That matters most for AI agents. Separate Google accounts are separate trust boundaries, and a write must never silently land in the wrong identity.
gw fixes exactly that and nothing else:
- Each profile gets its own
GOOGLE_WORKSPACE_CLI_CONFIG_DIR, so OAuth client config, encrypted credentials, token cache, and other cached runtime state never mix. - There is no default profile, no
GW_PROFILEfallback, and no--allmode. A missing profile is an error. gwrunsgwswith all three standard streams inherited, preserving stdin, stdout, streaming, interactivity, and binary output.gwmay add its own suggestion reminder to stderr. Exit codes are propagated unchanged, and a child killed by a signal is reported as128 + signal.
gw implements no Google API, no OAuth, no HTTP client, no daemon, and no output transformation.
nix profile install github:atqamz/gwOr in a flake:
{
inputs.gw.url = "github:atqamz/gw";
}The Nix package embeds the absolute store path of a pinned gws (currently 0.22.5), so gw never depends on whatever gws happens to be on $PATH. You do not install or manage gws yourself.
gw --version
# gw 0.1.0
# gws /nix/store/...-gws-0.22.5/bin/gwsNon-Nix builds (cargo build --release) fall back to gws from $PATH.
gw profile add personal --account user@example.com
gw personal auth login
gw personal gmail +triage
gw personal calendar +agenda
gw work tasks tasklists list
gw work drive files listEverything after the profile name is passed to gws verbatim.
gw profile add <name> --account <email>
gw profile list
gw profile list --json
gw profile show <name>
gw profile show <name> --jsonprofile list --json:
{"profiles":[{"name":"personal","account":"user@example.com","gws_config_dir":"/home/you/.config/gw/profiles/personal/gws"}]}There is no profile remove in v0.1. Once a profile owns OAuth credentials, token cache, and encryption state, deletion semantics are security sensitive and deserve a separate design.
$ gw gmail +triage
error: profile required
usage: gw <profile> <gws arguments...>gws names itself when it tells you what to run next, and it has no way to know it was invoked through gw.
Every such suggestion has to be read with the profile put back in:
gws prints |
run |
|---|---|
Run `gws auth login` to authenticate. |
gw personal auth login |
Run `gws auth setup` to configure |
gw personal auth setup |
try 'gws schema drive.files.list' |
gw personal schema drive.files.list |
Use `gws drive files list` to browse. |
gw personal drive files list |
Running the suggestion verbatim is not a harmless mistake.
gws on $PATH is a different, unprofiled installation whose config directory is ~/.config/gws, so the command would authenticate or write as whichever account happens to live there.
To make that hard to miss, gw writes one line to stderr after gws exits, whenever gws is likely to have suggested something:
$ gw personal auth setup
...
Setup complete! Run `gws auth login` to authenticate.
note: any `gws ...` command suggested above must be run as `gw personal ...`The note appears when the child exits non-zero, when the command is auth, schema, or generate-skills, and when --help or -h is present.
It is always on stderr, never on stdout, so --format json, --page-all, and binary responses are byte-identical to bare gws.
gw does not rewrite the suggestion in place.
The same text also reaches stdout inside structured payloads, for example as error.message in the JSON that gws emits on an authentication failure, and rewriting that would mean mutating data the caller parses.
Skill files written by gw <profile> generate-skills also contain bare gws examples.
Those are files on disk rather than output, so the note does not reach them; translate them when you adopt them.
~/.config/gw/
└── profiles/
├── personal/
│ ├── account
│ └── gws/
└── work/
├── account
└── gws/
The root honours $XDG_CONFIG_HOME and otherwise falls back to $HOME/.config.
account is non-secret metadata. gws/ is opaque: gw never reads, parses, prints, or migrates anything inside it.
- Profile names are validated against
[A-Za-z0-9._-], must not be empty, must not start with-or., and are capped at 64 characters..,.., path separators, absolute paths, and control characters are all rejected, and the resolved directory is checked to be a direct child of the profile root. - Profile directories are created
0700; theaccountfile is0600. - Profile metadata never becomes child environment variables. The only variable
gwsets isGOOGLE_WORKSPACE_CLI_CONFIG_DIR. - Identity-bearing
gwsvariables inherited from the ambient environment are removed before the child starts, so an ambient credential cannot leak across the profile boundary:GOOGLE_WORKSPACE_CLI_CONFIG_DIR,GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE,GOOGLE_WORKSPACE_CLI_TOKEN,GOOGLE_WORKSPACE_CLI_CLIENT_ID,GOOGLE_WORKSPACE_CLI_CLIENT_SECRET,GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND. Othergwsvariables such as logging and sanitisation settings are passed through. - Account metadata is sanitised on read and escaped on JSON output, so a tampered
accountfile cannot inject terminal escapes or break the JSON. gwnever has credential material in memory and never prints it.
After authenticating, verify the identity with a normal API call rather than by inspecting credential files.
skills/google-workspace/SKILL.md is a generic, model-agnostic Agent Skill. It contains no personal addresses and no machine-specific profile names.
bunx skills add atqamz/gw -g -a opencode -a claude-code -a codex --skill '*'Which profile means what for a given user belongs in local agent configuration, not in the shipped skill.
nix develop
cargo test
cargo clippy --all-targets -- -D warnings
nix fmt
nix flake checknix flake check builds the package, runs the test suite, and asserts that the built gw resolves the pinned gws store path.
The test suite uses a fake gws shell script, so isolation and pass-through are verified without Google credentials.
Releases are automated by release-please; merging the release PR tags a new version and updates CHANGELOG.md.
All Google Workspace functionality comes from googleworkspace/cli, Apache-2.0.
Related work: openclaw/gogcli offers native multi-account support and stronger built-in agent safety controls, and may be the better fit if you want broad Google access with multiple accounts today.
MIT