What
Let gog auth service-account set --key=... accept the service-account JSON key from stdin or an environment variable, the same way gog auth import already accepts the refresh token (--refresh-token-stdin / --refresh-token-file / --refresh-token-env) and gog auth tokens import already accepts - for stdin. Today service-account set is the only credential-intake path that's file-path-only — orchestrators that hold the SA key in memory (the natural shape for non-interactive deployment) must write it to a tempfile, manage cleanup with trap, and worry about the on-disk exposure surface between write and use.
Why
Today's intake shape
Three credential-intake commands, three different input contracts:
The pattern is otherwise consistent — every other intake path supports stdin. service-account set is the outlier.
Verified at internal/cmd/auth_service_account.go:
type AuthServiceAccountSetCmd struct {
Email string `arg:"" name:"email" required:""`
Key string `name:"key" required:"" help:"Path to service account JSON key file"`
}
func (c *AuthServiceAccountSetCmd) Run(ctx context.Context, flags *RootFlags) error {
// ...
keyPath := strings.TrimSpace(c.Key)
keyPath, err := config.ExpandPath(keyPath)
// ...
data, err := os.ReadFile(keyPath) // file-only
No - fallback for stdin, no --key-env, no --key-stdin.
Project-side cost
Service-account workspace domain-wide-delegation is the natural auth shape for headless workspace automation (CI jobs, agent runs, server-side renderers). The orchestrator typically holds the SA key in memory — pulled from a secret store, decrypted from a database column, passed via env from a deployment system. With --key=<path>-only:
- Tempfile dance is forced.
mktemp, trap 'rm -f' EXIT, write the JSON, hope the process didn't crash between write and cleanup, hope no other reader picked up the file in that window.
- On-disk exposure surface is non-zero. The tempfile sits at 0600 in
$TMPDIR for the duration of the call. Image leaks, crash dumps, monitoring agents that snapshot /tmp all capture it.
- Argv hygiene constraint partially defeated.
auth import was carefully designed to keep tokens out of /proc/<pid>/cmdline. service-account set lets you pass a path, but the content still has to land somewhere on disk first. Stdin / env routing closes that gap.
- API consistency. Operators wiring deployment automation across multiple
auth subcommands hit one "why is this one different" cliff. Predictable shapes lower the bar to correct, secure wiring.
Precedent already in this repo
auth import shipped these three input modes specifically for non-interactive deployment (release notes for v0.17.0):
Auth: add gog auth import --client --email with --refresh-token-stdin, --refresh-token-file, or --refresh-token-env for non-interactive token import without exposing secrets in argv
The same rationale applies one-for-one to service-account set. Same threat model, same intake shape, same desired ergonomics.
Suggested shape
Mirror auth import's flag set on service-account set:
# Today (still supported)
gog auth service-account set --key=/path/to/sa.json admin@example.com
# New: stdin
printf '%s' "$SA_KEY_JSON" | gog auth service-account set --key=- admin@example.com
# New: file (already supported; explicit naming for symmetry)
gog auth service-account set --key=/path/to/sa.json admin@example.com
# New: env
SA_KEY_JSON="$(read-from-vault)" gog auth service-account set --key-env=SA_KEY_JSON admin@example.com
Three possible flag patterns to consider:
Option A: extend --key to accept - for stdin (like auth credentials set):
... --key=- admin@example.com
Option B: separate flags mirroring auth import:
... --key-stdin admin@example.com
... --key-file=<path> admin@example.com
... --key-env=<VAR> admin@example.com
Option C: both — --key=- works (back-compat extension), the explicit --key-stdin / --key-env aliases for parity with auth import.
Option B is the cleanest mirror of auth import — exactly one source, named explicitly, exclusive of the others (auth_import.resolveRefreshToken validates "exactly one source" — same validation should apply here). Option A is the smallest change but groups three distinct concepts under one flag.
Maintainer's call. The load-bearing part is that stdin and env modes both exist, with the same argv-clean / no-tempfile guarantees auth import already provides.
Validation
Borrow the existing pattern from auth_import.go resolveRefreshToken:
- Exactly one source must be specified; conflicts return a usage error.
- Empty input is a parse error (mirrors
errMissingRefreshToken).
- Existing parser
parseServiceAccountJSON runs unchanged on the assembled []byte.
Scope
internal/cmd/auth_service_account.go: extend AuthServiceAccountSetCmd with the new flags + a resolveServiceAccountKey() helper that mirrors resolveRefreshToken()'s shape.
- No changes to storage shape — the parsed SA JSON still lands at
$XDG_CONFIG_HOME/gogcli/service-accounts/<email>.json via config.WriteFileAtomic.
- No changes to consumer code paths (
tokenSourceForServiceAccountScopes etc.).
Out of scope
- Moving the SA JSON itself to the keyring. The SA key is bulkier (~2KB JSON) than a refresh token and has a different threat model (single-tenant, infrequently rotated). Worth a separate proposal — file later if a deployment pattern hits the on-disk concern hard enough.
- Dropping the
--key=<path> mode. Tooling that downloads SA keys from Google Cloud Console naturally produces files; existing deployments rely on this. Pure addition, no removal.
Context
- Sibling commands
gog auth import (v0.17.0) and gog auth tokens import (- for stdin already supported) demonstrate @steipete already accepts this intake-shape pattern in this repo.
- Part of a small cluster of credential-handling proposals filed in the same week: #596 (move
client_secret to keyring), #597 (cross-process locking around keyring writes), #598 (access-token seeding in auth import), #599 (${VAR} interpolation in credentials.json). Independent of all four — this issue only touches auth service-account set's intake flags, no shared code surface — but completes the set of "non-interactive intake parity across auth subcommands."
- No existing issue, PR, or discussion proposes this — searched for
service-account set stdin, --key-env, --key-stdin, service account input; discussions are disabled on this repo. Filing as a clean new ask.
References
All references pinned to v0.17.0:
What
Let
gog auth service-account set --key=...accept the service-account JSON key from stdin or an environment variable, the same waygog auth importalready accepts the refresh token (--refresh-token-stdin/--refresh-token-file/--refresh-token-env) andgog auth tokens importalready accepts-for stdin. Todayservice-account setis the only credential-intake path that's file-path-only — orchestrators that hold the SA key in memory (the natural shape for non-interactive deployment) must write it to a tempfile, manage cleanup withtrap, and worry about the on-disk exposure surface between write and use.Why
Today's intake shape
Three credential-intake commands, three different input contracts:
gog auth import--refresh-token-stdin--refresh-token-env--refresh-token-filegog auth tokens import <inPath><inPath>=-<inPath>(positional)gog auth service-account set --key=<path>--key=<path>gog auth credentials set <path><path>=-<path>(positional)The pattern is otherwise consistent — every other intake path supports stdin.
service-account setis the outlier.Verified at
internal/cmd/auth_service_account.go:No
-fallback for stdin, no--key-env, no--key-stdin.Project-side cost
Service-account workspace domain-wide-delegation is the natural auth shape for headless workspace automation (CI jobs, agent runs, server-side renderers). The orchestrator typically holds the SA key in memory — pulled from a secret store, decrypted from a database column, passed via env from a deployment system. With
--key=<path>-only:mktemp,trap 'rm -f' EXIT, write the JSON, hope the process didn't crash between write and cleanup, hope no other reader picked up the file in that window.$TMPDIRfor the duration of the call. Image leaks, crash dumps, monitoring agents that snapshot/tmpall capture it.auth importwas carefully designed to keep tokens out of/proc/<pid>/cmdline.service-account setlets you pass a path, but the content still has to land somewhere on disk first. Stdin / env routing closes that gap.authsubcommands hit one "why is this one different" cliff. Predictable shapes lower the bar to correct, secure wiring.Precedent already in this repo
auth importshipped these three input modes specifically for non-interactive deployment (release notes for v0.17.0):The same rationale applies one-for-one to
service-account set. Same threat model, same intake shape, same desired ergonomics.Suggested shape
Mirror
auth import's flag set onservice-account set:Three possible flag patterns to consider:
Option A: extend
--keyto accept-for stdin (likeauth credentials set):Option B: separate flags mirroring
auth import:Option C: both —
--key=-works (back-compat extension), the explicit--key-stdin/--key-envaliases for parity withauth import.Option B is the cleanest mirror of
auth import— exactly one source, named explicitly, exclusive of the others (auth_import.resolveRefreshTokenvalidates "exactly one source" — same validation should apply here). Option A is the smallest change but groups three distinct concepts under one flag.Maintainer's call. The load-bearing part is that stdin and env modes both exist, with the same argv-clean / no-tempfile guarantees
auth importalready provides.Validation
Borrow the existing pattern from
auth_import.goresolveRefreshToken:errMissingRefreshToken).parseServiceAccountJSONruns unchanged on the assembled[]byte.Scope
internal/cmd/auth_service_account.go: extendAuthServiceAccountSetCmdwith the new flags + aresolveServiceAccountKey()helper that mirrorsresolveRefreshToken()'s shape.$XDG_CONFIG_HOME/gogcli/service-accounts/<email>.jsonviaconfig.WriteFileAtomic.tokenSourceForServiceAccountScopesetc.).Out of scope
--key=<path>mode. Tooling that downloads SA keys from Google Cloud Console naturally produces files; existing deployments rely on this. Pure addition, no removal.Context
gog auth import(v0.17.0) andgog auth tokens import(-for stdin already supported) demonstrate @steipete already accepts this intake-shape pattern in this repo.client_secretto keyring), #597 (cross-process locking around keyring writes), #598 (access-token seeding inauth import), #599 (${VAR}interpolation incredentials.json). Independent of all four — this issue only touchesauth service-account set's intake flags, no shared code surface — but completes the set of "non-interactive intake parity acrossauthsubcommands."service-account set stdin,--key-env,--key-stdin,service account input; discussions are disabled on this repo. Filing as a clean new ask.References
All references pinned to v0.17.0:
internal/cmd/auth_service_account.go—AuthServiceAccountSetCmd, the file-only intake path.internal/cmd/auth_import.go—resolveRefreshToken(), the model implementation for three input modes.internal/cmd/auth_tokens.go—auth tokens import, already supports-for stdin (different intake but same precedent).internal/cmd/auth_credentials.go—auth credentials set, already supports-for stdin.