A free, local, open-source macOS GUI for GAM7 — administer Google Workspace (users, groups, signatures, delegates, vacation responders, reports, and more) without memorizing CLI commands, with your credentials kept in the macOS Keychain.
GAM exposes far more of Google Workspace than the Admin Console surfaces (Gmail signatures/delegates/forwarding, advanced group settings, bulk operations, reporting). GamGUI puts a safe, native front end on top of it.
Actively developed and used against live Google Workspace tenants. Working today:
- Setup wizard — first-run GAM project / OAuth / domain-wide-delegation flow.
- Users — fast list/search/detail (cached + paginated), profile editing (title/department/location) with a bulk "assign store" tool, mailbox delegates, vacation responders, and a guarded suspend.
- Gmail signatures — a scoped designer with variables, a live preview, and bulk apply.
- Groups — membership management, including a drag-and-drop board.
- Calendars — find any shared calendar by name (instant, from a local index that scales to large tenants), see who has access, search a calendar's events, and remove a stray event or an entire orphaned secondary calendar.
- Lifecycle — a guided offboarding routine (reset password → delegate → auto-responder → transfer Drive & calendars → remove from everyone's calendars → reminder on the manager), with a live preview of the generated auto-reply.
- Reports — 2SV gaps, inactive accounts, admins, missing recovery, and directory completeness.
You build and run it yourself; it is not yet notarized for distribution to other Macs.
Destructive actions are guarded — but verify before trusting them on production. Suspend, account delete, calendar/event delete, data transfer, the offboarding routine, and bulk operations all run behind a preview → typed confirmation → audit-logged path. A few of the newer ones haven't yet been exercised against a live tenant, so run them once on a throwaway test user/event before relying on them. Account deletion is reversible only within Google's ~20-day window. GamGUI is provided as-is under the MIT License, with no warranty — use at your own risk; you are responsible for what you run against your own tenant.
- Local & native — a single bundled
.app; no server, nothing leaves your machine. - Secure — secrets live in the macOS Keychain; GAM's plaintext credential files are
materialized into a locked-down temporary directory only for the duration of each
gaminvocation, then wiped. (details) - Easy but powerful — form/table UI for the common painful tasks, full GAM power underneath.
- Connector-ready — built around a connector protocol, so the Google Workspace connector is cleanly isolated and other systems could be added later without touching the UI.
HTMX views → FastAPI routes → Services → Connector protocol → GAMConnector
→ GAMRunner (subprocess)
→ SecretsVault (Keychain) + EphemeralConfig (temp GAMCFGDIR)
Wrapped in a pywebview native window (WKWebView). See docs/the plan for the full design.
GAM stores credentials as plaintext files (client_secrets.json, oauth2.txt,
oauth2service.json) in its config dir. oauth2service.json can impersonate any user in the
domain and oauth2.txt is effectively an admin password, so GamGUI:
- keeps the canonical copies in the Keychain (
keyring, device-bound, not synced); - materializes them into a
chmod 700temp dir (fileschmod 600) set asGAMCFGDIRonly for eachgamcall; - wipes that dir on completion (success or failure);
- writes refreshed OAuth tokens back to the Keychain.
Requirements: Python 3.9+ and macOS (to run the native window; the test suite itself runs on Linux too). No Google credentials are needed to build or test.
git clone <repo-url> && cd gamgui
make setup # create .venv, install dev + native-window deps
make gam # vendor the pinned GAM7 binary into gamgui/resources/gam7 (needs network)
make test # offline test suite — uses a mock gam, no binary/credentials required
make run # launch the app (native window; prints a browser URL if pywebview is absent)make help lists all targets. Prefer raw commands? pip install -e ".[dev,desktop]", then
scripts/fetch_gam.sh, pytest, python -m gamgui.app. For an exact pinned install instead of the
flexible one, use pip install -r requirements.txt.
The GAM7 binary is not committed (platform-specific, large) — make gam / scripts/fetch_gam.sh
fetches the pinned version (v7.46.02) from the official releases and records its checksum.
make app # PyInstaller -> dist/GamGUI.app (bundles Python + the GAM7 binary)For distribution to other Macs you must codesign + notarize the bundle (including the embedded gam binary); running it yourself needs no signing.
By default the app is ad-hoc signed, so macOS treats each rebuild as a new identity and
re-prompts for the Keychain on every launch — and "Always Allow" never sticks. The fix (no Apple
Developer account needed — that's only for shipping the app to other people's Macs) is a stable
self-signed code-signing cert named GamGUI Local. Once it exists in your login keychain,
scripts/build_app.sh signs with it automatically, so your one-time Always Allow persists
across launches and rebuilds.
Create it once, either way:
- GUI: Keychain Access → Certificate Assistant → Create a Certificate… → name it
GamGUI Local, Identity Type Self-Signed Root, Certificate Type Code Signing. - CLI:
D=$(mktemp -d) printf '[req]\ndistinguished_name=dn\nx509_extensions=v3\nprompt=no\n[dn]\nCN=GamGUI Local\n[v3]\nbasicConstraints=critical,CA:false\nkeyUsage=critical,digitalSignature\nextendedKeyUsage=critical,codeSigning\n' > "$D/c.cnf" openssl req -x509 -newkey rsa:2048 -keyout "$D/k.pem" -out "$D/c.pem" -days 3650 -nodes -config "$D/c.cnf" openssl pkcs12 -export -inkey "$D/k.pem" -in "$D/c.pem" -out "$D/id.p12" -passout pass:gamgui-local -name "GamGUI Local" security import "$D/id.p12" -P gamgui-local -T /usr/bin/codesign && rm -rf "$D"
Then rebuild (make app). The first launch still asks once per credential — click Always
Allow on each — and you won't be prompted again, even after future rebuilds. (Override the cert
name with CODESIGN_IDENTITY=…. The cert is local and not trusted for distribution by design — it
only quiets your own Keychain.)
The app also caches the three secrets in-process for a sliding window (default 5 min) so a burst of
actions doesn't re-prompt; tune with GAMGUI_SECRET_CACHE_TTL (seconds; 0 disables).
GamGUI pins a tested GAM7 version — EXPECTED_GAM_VERSION in gamgui/core/gam/commands.py, matched by
scripts/fetch_gam.sh. It never auto-upgrades; you bump deliberately, and three guards keep that safe:
- Release-watch (
.github/workflows/gam-watch.yml, weekly) opens an issue when GAM ships a version newer than the pin — awareness, not auto-upgrade. - Compat check (
gam-compatCI job +tests/test_command_contract.py) asserts every GAM sub-command our builders use still exists in the vendored command reference, so a renamed/removed command fails CI rather than your tenant. A non-blockinggam-latest-previewjob runs the same check against the newest GAM as an early warning. - Runtime self-check — if the running
gamdiffers from the tested version (e.g. aGAMGUI_GAM_BINARYoverride), the setup screen shows a soft warning. It never blocks.
To bump GAM:
make gam --tag vX.Y.Z— re-vendor the binary, command reference, and checksum.- Update
EXPECTED_GAM_VERSION(gamgui/core/gam/commands.py) andTAG(scripts/fetch_gam.sh). make test— the command-contract test flags any sub-command that changed.- Skim
gamgui/resources/gam7/GamUpdate.txtfor breaking changes. .venv/bin/python scripts/acceptance.pyagainst a tenant — read-only; the true output-shape check.- Commit.
pytest is fully offline (mock gam + in-memory Keychain). CI runs it on Ubuntu and macOS across
Python 3.9 and 3.12 — see .github/workflows/ci.yml.
The Signatures screen designs one HTML signature with variables, previews it rendered for a real person, and applies it in bulk — scoped to a single user (for testing), a group, an org unit, a department, a location, or the whole company. Each user's current signature is also shown rendered on their detail page.
Template variables (filled per user from the directory):
{name} {first} {last} {email} {title} ({role} is an alias) {phone} {department}
{location} {ou}. Wrap a fragment in [[ … ]] to drop it when a variable inside is empty — e.g.
[[{title} · ]] vanishes for people with no title, so one template can roll out before every profile
is filled in.
Gmail does not allow inline/base64 images or Google Drive links in signatures — every image must
be a file at a public HTTPS URL. GamGUI is a local app and doesn't host images itself; you point
the template's <img src="…"> at wherever you host them. Whatever host you choose, the URL must be:
- HTTPS and anonymously reachable — Gmail fetches images through its own proxy (no cookies/referer) and caches them. Test a URL in a private/incognito window; if it loads there, Gmail can fetch it.
- served with the correct
Content-Type(image/png, …) and no hotlink/referer protection — referer-based protection is the usual cause of "the logo shows for me but not for recipients." - versioned by filename when an image changes (
logo-2026.png) — Gmail caches by URL, so overwriting the same name can keep serving the old one.
Size icons ~2× their display size and set explicit width/height on each <img>.
Where to host — pick one:
- A web host you already have (simplest). Drop the files in a public folder, e.g.
https://yourdomain.com/email/logo.png. Done. - Google Cloud Storage (Google-native; reuse the GCP project GAM created). Requires a billing
account linked to the project — but small signature assets fall under the Always-Free tier, so
the bill rounds to $0:
- Cloud Console → Billing → link a billing account to the project (if not already).
- Cloud Storage → Create bucket — globally-unique name, a US region, Standard class, Uniform bucket-level access.
- Make objects public: bucket Permissions → Grant access → principal
allUsers→ roleStorage Object Viewer. (If your org enforces Public access prevention, allow it on this bucket.) - Upload the images.
- Reference them at
https://storage.googleapis.com/<bucket>/<path>/logo.png. (Pricing changes — confirm the current free-tier limits, but for a handful of small PNGs it is effectively free.)
- GitHub + jsDelivr (free, no billing). Commit the images to a public repo and serve them via the
jsDelivr CDN:
https://cdn.jsdelivr.net/gh/<user>/<repo>@<branch>/path/logo.png. CDN-fast, no card. - Cloudflare R2 / Amazon S3 — or any public-object store — also work.
MIT — see LICENSE.