You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Let gog auth credentials set resolve ${VAR} / ${VAR:-fallback} placeholders against the process environment when parsing the input JSON. Today the parser does plain json.Unmarshal with no expansion — operators delivering credentials via env-var must shell-substitute on the writer side (e.g. jq -cn '{...env.VAR...}'), which adds a setup-time tool dependency and a hand-rolled escaping concern. Other openclaw projects (mcporter) already support universal ${VAR} interpolation in config; gogcli is the asymmetric outlier.
Why
Today's intake shape
gog auth credentials set <path|-> reads bytes from a file or stdin, then calls ParseGoogleOAuthClientJSON:
Plain json.Unmarshal. Grepping the entire repo confirms:
grep -r "os.Expand\|ExpandEnv" / → 0 hits in the credentials path
No string-interpolation helper anywhere in internal/config/
So a credentials.json with "client_secret": "${MY_SECRET}" is taken literally — gogcli stores the literal string ${MY_SECRET} as the OAuth client secret, and the next API call against Google fails with invalid_client.
Project-side cost
For headless / agentic deployments, the natural shape is "operator-side already has the secret in an env var; pass it through to gogcli." Today that requires:
Shell substitution before invocation — jq -cn or envsubst or printf with manual JSON-escaping. Adds a setup-time runtime dep beyond gogcli itself. (Verified: jq is the usual choice; it's pre-installed in many runtimes but not universally.)
Escaping discipline. If the OAuth client_secret contains a backslash, a control character, or a double-quote, naive printf produces invalid JSON and the call fails opaquely. jq -n handles this; printf doesn't.
Bigger init scripts. A non-trivial setup.sh per skill instead of a credentials.json template that gogcli reads directly.
Precedent: mcporter already does this
openclaw/mcporter (also maintained by @steipete) interpolates ${VAR} on every string-valued config field at request time, with ${VAR:-fallback} defaults. Documented at the mcporter config docs:
universal ${VAR} and ${VAR:-fallback} interpolation on every string-valued field on v0.11.0+
That's how mcporter.json templates work end-to-end: ship a config with ${VAR} placeholders, gogcli's sibling resolves them lazily against the process env. Skills shipping mcporter wrappers rely on this — there is no init script writing tokens to disk because mcporter does the interpolation.
The intake shape gogcli is missing is exactly the one mcporter has — @steipete's design taste from the sibling project, just not yet applied to credentials.json.
Suggested shape
Resolve ${VAR} and ${VAR:-fallback} against os.Environ() during credentials-JSON extraction inside ParseGoogleOAuthClientJSON. Apply only to string-valued positions — not to keys or non-string values. (Pre-parse byte substitution was considered and rejected; see § "Implementation sketch".)
# Today's verbose path
jq -cn '{web: {client_id: env.CID, client_secret: env.CSEC}}' \
| gog auth credentials set - --client=mine
# Post-feature: a static template, env-var values# (--expand-env shown explicitly per the opt-in-then-default rollout in# § "Toggle" below; drop the flag if the rollout lands always-on.)
cat <<'EOF' | gog auth credentials set - --client=mine --expand-env{ "web": { "client_id": "${GOG_CLIENT_ID}", "client_secret": "${GOG_CLIENT_SECRET}" }}EOF
Implementation sketch
Shape B (proposed) is below. Shape A is documented first as an alternative considered and rejected, so reviewers don't ask "why not pre-parse?"
Shape A (alternative — rejected): pre-parse byte sweep. Walk the raw bytes; replace ${VAR} / ${VAR:-fallback} patterns inline; pass the substituted bytes to json.Unmarshal. Doesn't match the strings-only contract — a raw byte sweep would also expand placeholders inside JSON keys, structural positions (commas, braces, arrays), and inside strings that happen to contain ${...} literally. Making it token-aware (so it expands only inside JSON-string values) reproduces most of json.Unmarshal's work, defeating the "simple inline replacement" advantage. Mentioned here only to preempt the "why not pre-parse?" review question; not the proposed implementation.
Shape B: expand during extraction. Inside ParseGoogleOAuthClientJSON (internal/config/credentials.go), json.Unmarshal into the existing googleCredentialsFile envelope first, then run os.Expand (with a custom mapper for the :- default syntax) on the two extracted string fields before returning the ClientCredentials:
Risks: doesn't catch interpolation that produces structural JSON (e.g. ${ARRAY_OF_DOMAINS} expanding to ["a", "b"]). Mitigation: don't promise that — strings only, by design.
Shape B enforces the strings-only contract structurally — json.Unmarshal does the JSON parsing; expansion runs on already-extracted string fields, so it cannot accidentally produce structural JSON or expand inside keys. Matches mcporter's "universal ${VAR} on every string-valued field" wording; smallest surgical change to the existing extraction path (two expandEnv calls between extract and check).
Strict-by-default
Missing env var without a :-fallback → parse error. Don't silently substitute empty string. Matches mcporter's semantics (mcporter v0.11.0+ env-resolution).
Toggle
Could be opt-in via flag (--expand-env) or always-on. Always-on is simpler and matches mcporter, but has a sharper back-compat edge: any existing credentials.json containing a literal ${...} substring (vanishingly unlikely in real Google-issued OAuth client JSON, but technically possible) would break.
Pragmatic answer: opt-in via flag for the first release, then flip to default after one release cycle once any edge cases shake out. Mirrors how a careful maintainer would land the change.
Scope
internal/config/credentials.go: add the env-expansion pass (Shape B above) inside or just before ParseGoogleOAuthClientJSON.
Optional new flag on gog auth credentials set (--expand-env).
No changes to storage shape — the resolved {client_id, client_secret} lands on disk exactly as today (or in the keyring once #596 merges).
Out of scope
Interpolation in other on-disk config files (config.json, paired.json, etc.). Useful, but each has its own threat model — credentials.json is the one that operators hand-write or feed via stdin during deployment automation; the others are gogcli-managed.
Expansion at read time instead of write time. Doing it at read time means env can change between auth credentials set and the next API call; surprising. Resolve once, persist the resolved value.
mcporter's interpolation feature shipped in v0.11.0+; the implementation is straightforward Go (~30 LoC for the helper + tests).
No existing issue, PR, or discussion proposes this — searched for env interpolation credentials, ${VAR} credentials.json, expand env auth, envsubst credentials; discussions are disabled on this repo. Filing as a clean new ask.
References
All references pinned to v0.17.0 (gogcli) and v0.11.1 (mcporter, as cited in third-party guides):
What
Let
gog auth credentials setresolve${VAR}/${VAR:-fallback}placeholders against the process environment when parsing the input JSON. Today the parser does plainjson.Unmarshalwith no expansion — operators delivering credentials via env-var must shell-substitute on the writer side (e.g.jq -cn '{...env.VAR...}'), which adds a setup-time tool dependency and a hand-rolled escaping concern. Other openclaw projects (mcporter) already support universal${VAR}interpolation in config; gogcli is the asymmetric outlier.Why
Today's intake shape
gog auth credentials set <path|->reads bytes from a file or stdin, then callsParseGoogleOAuthClientJSON:Plain
json.Unmarshal. Grepping the entire repo confirms:grep -r "os.Expand\|ExpandEnv" /→ 0 hits in the credentials pathinternal/config/So a credentials.json with
"client_secret": "${MY_SECRET}"is taken literally — gogcli stores the literal string${MY_SECRET}as the OAuth client secret, and the next API call against Google fails withinvalid_client.Project-side cost
For headless / agentic deployments, the natural shape is "operator-side already has the secret in an env var; pass it through to gogcli." Today that requires:
jq -cnorenvsubstorprintfwith manual JSON-escaping. Adds a setup-time runtime dep beyond gogcli itself. (Verified:jqis the usual choice; it's pre-installed in many runtimes but not universally.)client_secretcontains a backslash, a control character, or a double-quote, naiveprintfproduces invalid JSON and the call fails opaquely.jq -nhandles this;printfdoesn't.setup.shper skill instead of acredentials.jsontemplate that gogcli reads directly.Precedent: mcporter already does this
openclaw/mcporter(also maintained by @steipete) interpolates${VAR}on every string-valued config field at request time, with${VAR:-fallback}defaults. Documented at the mcporter config docs:That's how
mcporter.jsontemplates work end-to-end: ship a config with${VAR}placeholders, gogcli's sibling resolves them lazily against the process env. Skills shipping mcporter wrappers rely on this — there is no init script writing tokens to disk because mcporter does the interpolation.The intake shape gogcli is missing is exactly the one mcporter has — @steipete's design taste from the sibling project, just not yet applied to
credentials.json.Suggested shape
Resolve
${VAR}and${VAR:-fallback}againstos.Environ()during credentials-JSON extraction insideParseGoogleOAuthClientJSON. Apply only to string-valued positions — not to keys or non-string values. (Pre-parse byte substitution was considered and rejected; see § "Implementation sketch".)Implementation sketch
Shape B (proposed) is below. Shape A is documented first as an alternative considered and rejected, so reviewers don't ask "why not pre-parse?"
Shape A (alternative — rejected): pre-parse byte sweep. Walk the raw bytes; replace
${VAR}/${VAR:-fallback}patterns inline; pass the substituted bytes tojson.Unmarshal. Doesn't match the strings-only contract — a raw byte sweep would also expand placeholders inside JSON keys, structural positions (commas, braces, arrays), and inside strings that happen to contain${...}literally. Making it token-aware (so it expands only inside JSON-string values) reproduces most ofjson.Unmarshal's work, defeating the "simple inline replacement" advantage. Mentioned here only to preempt the "why not pre-parse?" review question; not the proposed implementation.Shape B: expand during extraction. Inside
ParseGoogleOAuthClientJSON(internal/config/credentials.go),json.Unmarshalinto the existinggoogleCredentialsFileenvelope first, then runos.Expand(with a custom mapper for the:-default syntax) on the two extracted string fields before returning theClientCredentials:Risks: doesn't catch interpolation that produces structural JSON (e.g.
${ARRAY_OF_DOMAINS}expanding to["a", "b"]). Mitigation: don't promise that — strings only, by design.Shape B enforces the strings-only contract structurally —
json.Unmarshaldoes the JSON parsing; expansion runs on already-extracted string fields, so it cannot accidentally produce structural JSON or expand inside keys. Matches mcporter's "universal${VAR}on every string-valued field" wording; smallest surgical change to the existing extraction path (twoexpandEnvcalls between extract and check).Strict-by-default
Missing env var without a
:-fallback→ parse error. Don't silently substitute empty string. Matches mcporter's semantics (mcporter v0.11.0+env-resolution).Toggle
Could be opt-in via flag (
--expand-env) or always-on. Always-on is simpler and matches mcporter, but has a sharper back-compat edge: any existingcredentials.jsoncontaining a literal${...}substring (vanishingly unlikely in real Google-issued OAuth client JSON, but technically possible) would break.Pragmatic answer: opt-in via flag for the first release, then flip to default after one release cycle once any edge cases shake out. Mirrors how a careful maintainer would land the change.
Scope
internal/config/credentials.go: add the env-expansion pass (Shape B above) inside or just beforeParseGoogleOAuthClientJSON.gog auth credentials set(--expand-env).{client_id, client_secret}lands on disk exactly as today (or in the keyring once #596 merges).Out of scope
config.json,paired.json, etc.). Useful, but each has its own threat model —credentials.jsonis the one that operators hand-write or feed via stdin during deployment automation; the others are gogcli-managed.auth credentials setand the next API call; surprising. Resolve once, persist the resolved value.Context
client_secretto keyring). The two issues are complementary — Feature: store Google OAuth client_secret in the keyring (parity with the Zoom path) #596 changes where the secret lives; this issue changes how it gets in. They don't conflict.env interpolation credentials,${VAR} credentials.json,expand env auth,envsubst credentials; discussions are disabled on this repo. Filing as a clean new ask.References
All references pinned to v0.17.0 (gogcli) and v0.11.1 (mcporter, as cited in third-party guides):
internal/config/credentials.go—ParseGoogleOAuthClientJSON, the function this issue extends.internal/cmd/auth_credentials.go—gog auth credentials setinvokes the parser; new flag plumbing lives here.os.Expand— supports${VAR}syntax via a custom mapper function; the natural building block for Shape B above.${VAR}design and rationale.