From gws-multi-account
Run the gws CLI (Google Workspace CLI) against multiple accounts stored under ~/.config/gws/<email>/, with metadata in ~/.config/gws/accounts.json. Always load this skill when executing any gws command so the right account is selected. Covers account selection, adding new accounts, migrating legacy flat ~/.config/gws/ setups, and the non-blocking `gws auth login` OAuth flow (background-spawn + poll; never run `gws auth login` in the foreground from an agent).
How this skill is triggered — by the user, by Claude, or both
Slash command
/gws-multi-account:gws-multi-accountThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The [gws CLI](https://github.com/googleworkspace/cli) ships with a single-config-dir model: all credentials live under one directory. This skill layers a **multi-account convention** on top:
The gws CLI ships with a single-config-dir model: all credentials live under one directory. This skill layers a multi-account convention on top:
~/.config/gws/accounts.json — account registry (email + description).~/.config/gws/<email>/ — one config dir per account, holding client_secret.json, credentials.enc, token_cache.json, and cache/.gws v0.4.2+ uses ~/.config/gws on all platforms (macOS, Linux, Windows) — on Windows this resolves to %USERPROFILE%\.config\gws (e.g., C:\Users\alice\.config\gws). If you're on a pre-v0.4.2 install and your data is still under %APPDATA%\gws, migrate it or let gws continue using the legacy path (it auto-detects).
Every gws invocation must set GOOGLE_WORKSPACE_CLI_CONFIG_DIR to the per-account directory explicitly. There is no implicit default — pick an account every time.
Shell syntax note. Examples below use POSIX shell (bash/zsh) — they work on macOS, Linux, and in any POSIX-compatible shell on Windows (Git Bash, WSL). For PowerShell and cmd.exe, see the Selecting an account section for syntax equivalents.
~/.config/gws/ ← %USERPROFILE%\.config\gws on Windows
├── accounts.json ← registry (this skill's contract)
├── [email protected]/ ← one dir per account, named by email
│ ├── client_secret.json
│ ├── credentials.enc
│ ├── token_cache.json
│ └── cache/
└── [email protected]/
└── …
Flat object keyed by email. Each value is metadata for that account.
{
"[email protected]": {
"description": "Personal Gmail"
},
"[email protected]": {
"description": "Work (Acme Corp) — billing, calendar"
}
}
~/.config/gws/.description is a free-form human-readable hint. Use it when the user refers to an account by nickname ("work", "personal", "the client one") to pick the right email.default field. Callers always specify an account.~/.config/gws/accounts.json.description + email local-part.
"personal" → match the entry whose description contains "personal" (or @gmail.com local email)."work" / company name → match by description.accounts.json.GOOGLE_WORKSPACE_CLI_CONFIG_DIR="$HOME/.config/gws/<email>" for the command. Use $HOME, never a literal ~ — ~ does not expand inside quoted values or when the command isn't run through a shell, and gws will then create a stray ~/.config/gws/... directory under $PWD.cat ~/.config/gws/accounts.json
GOOGLE_WORKSPACE_CLI_CONFIG_DIR="$HOME/.config/gws/[email protected]" \
gws gmail users messages list --params '{"userId":"me"}'
Get-Content "$env:USERPROFILE\.config\gws\accounts.json"
$env:GOOGLE_WORKSPACE_CLI_CONFIG_DIR = "$env:USERPROFILE\.config\gws\[email protected]"
gws gmail users messages list --params '{"userId":"me"}'
type "%USERPROFILE%\.config\gws\accounts.json"
set "GOOGLE_WORKSPACE_CLI_CONFIG_DIR=%USERPROFILE%\.config\gws\[email protected]" && gws gmail users messages list --params "{\"userId\":\"me\"}"
Works identically across every gws service (gmail, calendar, drive, sheets, docs, slides, tasks, people, chat, forms, etc.).
gws auth login is an interactive OAuth2 flow that starts a localhost callback server and blocks until the browser redirects back. Agent shells typically time out around 60 seconds; a foreground gws auth login will be killed mid-flow, the callback server dies with it, and the URL you already showed the user redirects to a dead port.
Rule: never run gws auth login in the foreground from an agent. Background-spawn it, extract the OAuth URL from its log file, share the URL verbatim, and poll gws auth status for completion.
The full six-step flow (with macOS / Linux + Windows PowerShell commands, troubleshooting table, and scope-flag reference) lives in references/auth-login.md. Load it whenever the user asks to log in, re-authenticate, or when gws auth status shows token_valid: false.
Triggers that should make you read the reference: "log into gws", "gws re-auth", "gmail login expired", "구글 로그인", "gws 재인증", gws auth login.
GOOGLE_WORKSPACE_CLI_CONFIG_DIR explicitly. Never rely on a shell default or an exported variable — the invocation must be self-contained.~ in the env var value. Use "$HOME/.config/gws/<email>" on POSIX, "$env:USERPROFILE\.config\gws\<email>" on PowerShell, "%USERPROFILE%\.config\gws\<email>" on cmd. A literal ~ does not expand when quoted or when the command is run outside an interactive shell — gws will silently create a stray ~/.config/gws/... directory under $PWD instead of writing to the real home directory. The gws-multi-account hook blocks bare ~ values with an explanatory error.accounts.json, ask.accounts.json silently. Only modify it during the migration / add-account flows below, and only with the user's confirmation.credentials.enc, token_cache.json, .encryption_key).When the user wants to register a new account:
accounts.json key).mkdir -p ~/.config/gws/<email>New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.config\gws\<email>"gws auth login flow against that directory. If the user already has a client_secret.json, drop it into ~/.config/gws/<email>/client_secret.json first. Do not run gws auth login in the foreground — it starts a local OAuth callback server that dies when the agent's command timeout fires, stranding the user with a dead URL. Follow the background-spawn flow in references/auth-login.md instead.accounts.json (preserve existing entries). Use Node for cross-platform JSON merging — no jq dependency. Node is guaranteed present on any machine running Claude Code or opencode.
GWS_ACCOUNTS_JSON="$HOME/.config/gws/accounts.json" EMAIL=<email> DESC=<description> \
node -e "const fs=require('fs'),p=process.env.GWS_ACCOUNTS_JSON;const d=fs.existsSync(p)?JSON.parse(fs.readFileSync(p,'utf8')):{};d[process.env.EMAIL]={description:process.env.DESC};fs.writeFileSync(p,JSON.stringify(d,null,2)+'\n');"
GOOGLE_WORKSPACE_CLI_CONFIG_DIR="$HOME/.config/gws/<email>" \
gws gmail users getProfile --params '{"userId":"me"}'
If ~/.config/gws/ contains credential files at the root (not inside an <email>/ subdirectory), it's a legacy single-account setup. Migrate it before using this skill.
On Windows, pre-v0.4.2 gws installs stored the config at %APPDATA%\gws instead of %USERPROFILE%\.config\gws. If you find credentials there, treat it as the legacy flat layout too — migrate into ~/.config/gws/<email>/ (which resolves to %USERPROFILE%\.config\gws\<email>) so newer gws versions pick it up.
Legacy layout is present when any of these exist directly under ~/.config/gws/ (or %APPDATA%\gws\ on older Windows installs):
client_secret.jsoncredentials.enctoken_cache.json.encryption_keyCheck with:
ls ~/.config/gws/client_secret.json ~/.config/gws/credentials.enc \
~/.config/gws/token_cache.json ~/.config/gws/.encryption_key 2>/dev/null
Get-ChildItem "$env:USERPROFILE\.config\gws\client_secret.json","$env:USERPROFILE\.config\gws\credentials.enc",`
"$env:USERPROFILE\.config\gws\token_cache.json","$env:USERPROFILE\.config\gws\.encryption_key",`
"$env:APPDATA\gws\client_secret.json","$env:APPDATA\gws\credentials.enc",`
"$env:APPDATA\gws\token_cache.json","$env:APPDATA\gws\.encryption_key" -ErrorAction SilentlyContinue
Walk the user through this step by step. Do not skip the confirmation.
Detect and report. Tell the user: "I found a legacy gws config at ~/.config/gws/. I'd like to move it into the per-account layout."
Identify the account. Run a cheap call against the existing config to learn which email it belongs to:
gws gmail users getProfile --params '{"userId":"me"}'
Confirm the returned emailAddress with the user: "This config belongs to <email> — correct?"
Ask for a description. "How should I describe this account in accounts.json? (e.g., 'Personal Gmail', 'Work — Acme')."
Show the plan and confirm. Print what will happen before doing it:
mkdir -p ~/.config/gws/<email>client_secret.json, credentials.enc, token_cache.json, .encryption_key, and cache/ into ~/.config/gws/<email>/.accounts.json with { "<email>": { "description": "<description>" } }.~/.config/gws/.gitignore (if present) untouched at the root.Execute after the user confirms.
macOS / Linux:
EMAIL="<email>"
DESC="<description>"
mkdir -p ~/.config/gws/"$EMAIL"
for f in client_secret.json credentials.enc token_cache.json .encryption_key cache; do
if [ -e ~/.config/gws/"$f" ]; then
mv ~/.config/gws/"$f" ~/.config/gws/"$EMAIL"/
fi
done
GWS_ACCOUNTS_JSON="$HOME/.config/gws/accounts.json" EMAIL="$EMAIL" DESC="$DESC" \
node -e "const fs=require('fs'),p=process.env.GWS_ACCOUNTS_JSON;const d=fs.existsSync(p)?JSON.parse(fs.readFileSync(p,'utf8')):{};d[process.env.EMAIL]={description:process.env.DESC};fs.writeFileSync(p,JSON.stringify(d,null,2)+'\n');"
Windows (PowerShell):
$Email = "<email>"
$Desc = "<description>"
# Source: "$env:APPDATA\gws" for pre-v0.4.2 installs, "$env:USERPROFILE\.config\gws" otherwise.
$Source = "$env:USERPROFILE\.config\gws"
# Always target ~/.config/gws on Windows too — that's where gws v0.4.2+ looks.
$Target = "$env:USERPROFILE\.config\gws\$Email"
New-Item -ItemType Directory -Force -Path $Target | Out-Null
foreach ($f in 'client_secret.json','credentials.enc','token_cache.json','.encryption_key','cache') {
$src = Join-Path $Source $f
if (Test-Path $src) { Move-Item $src (Join-Path $Target $f) }
}
$env:GWS_ACCOUNTS_JSON = "$env:USERPROFILE\.config\gws\accounts.json"
$env:EMAIL = $Email
$env:DESC = $Desc
node -e "const fs=require('fs'),p=process.env.GWS_ACCOUNTS_JSON;const d=fs.existsSync(p)?JSON.parse(fs.readFileSync(p,'utf8')):{};d[process.env.EMAIL]={description:process.env.DESC};fs.writeFileSync(p,JSON.stringify(d,null,2)+'\n');"
Verify the migration. Use the platform-appropriate command from Selecting an account above and expect the same emailAddress as before.
Tell the user they should now invoke gws with GOOGLE_WORKSPACE_CLI_CONFIG_DIR="$HOME/.config/gws/<email>" on every call (a literal ~ won't expand when quoted — use $HOME). Bare gws ... without the env var will no longer find credentials.
<email>/ directories exist, ask the user which source is authoritative before doing anything. Do not merge blindly.accounts.json exists but an account directory is missing its credentials.enc, the account is unauthorized — re-run gws auth login against that config dir.~/.config/gws/ that is not listed in accounts.json, offer to add it (ask for a description) rather than deleting it.~/.config/gws/<email>/ contains live OAuth credentials. Never commit, copy to shared locations, or include in logs..gitignore at ~/.config/gws/.gitignore keeps credentials.enc, token_cache.json, .encryption_key, and cache/ untracked. Preserve it during migration.accounts.json contains only emails and descriptions — safe to back up, but avoid committing it if descriptions reveal sensitive context.Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub indentcorp/gws-multi-account --plugin gws-multi-account