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
Status: needs cross-platform design. Captures the problem and a tried alternative. The Windows + OneDrive story is the open question; do not implement until that's settled.
Problem
Every code flavor's devcontainer exposes the host's GitHub credential via:
(see internal/flavors/gocli/templates/.devcontainer/devcontainer.json.tmpl:19 and the equivalent in fullstack, gobackend, iac — and in this repo's own .devcontainer/devcontainer.json:19.)
In practice this is brittle for agentic work:
Stale values shadow the live login. If GH_TOKEN is set in the host shell from an earlier session and expires or rotates, the container inherits the dead token. The token from the keyring (which gh auth login populated) is ignored because GH_TOKEN takes precedence in gh. The agent sees gh auth status: The token in GH_TOKEN is invalid. and every subsequent gh / GitHub MCP call fails with Bad credentials, even though gh auth login was performed in the container.
This was hit live in the session that prompted this issue: the agent had to unset GH_TOKEN to get gh working at all. Once unset, gh auth status reported Active account: true against the keyring login.
Embedded tokens drift into git remote URLs. When GH_TOKEN is set, gh repo clone and friends embed the token in the cloned origin URL (https://x-access-token:ghp_...@github.com/...). The URL persists after the token is revoked, so future git push fails authentication even though the keyring login works. Cleanup requires the user to know to reset origin.
No re-auth path in-container.GH_TOKEN-mode gh will not let the user gh auth login over it — the env var wins. The user has to leave the container, reset the host env var, and rebuild.
One credential mechanism, two clients.gh itself can use either env var or keyring. The GitHub MCP server expects an env var. We are conflating them by routing both through GH_TOKEN, which is exactly where the stale-shadowing bug bites.
The net effect: agents working inside an agent-init devcontainer hit GitHub-auth dead ends often enough that troubleshooting it is a recurring tax.
What seems to work better
Mount the host's gh config directory into the container instead of passing the token as an env var:
gh inside the container uses the same auth state as the host. The keyring login made on the host (gh auth login) is visible to the container immediately. No env var, no shadowing.
Token rotation is automatic: gh auth refresh on the host updates the same file the container reads.
The GitHub MCP server can still get a credential via export GITHUB_TOKEN="$(gh auth token)" from the live state, which is the pattern docs/cli.md (the add-tracker section) already recommends.
No embedded tokens in cloned git URLs (because gh is the auth source, not an env var).
Open design questions — the load-bearing ones
Cross-platform source path for the mount.${localEnv:HOME}/.config/gh is correct on macOS and Linux. On Windows, the host config path differs:
WSL2 host: ~/.config/gh works (Linux file system).
Native Windows host (Git Bash / PowerShell): %USERPROFILE%\AppData\Roaming\GitHub CLI\ (per gh docs), which ${localEnv:HOME} does not resolve to.
${localEnv:USERPROFILE} exists on Windows but the path inside is different again.
Devcontainer JSON does not have conditional mounts. Options: (a) document per-platform mount strings and let the user pick, (b) ship the mount only when a ${localEnv:GH_CONFIG_DIR} variable is set and rely on a host-side helper to set it, (c) skip the mount on Windows and fall back to gh auth login in-container with no env var (the keyring lives in the container, but is wiped on rebuild).
OneDrive and other shared-folder hosts. The doc-collab flavors (claude-cowork, project-management) target users whose home folder may be inside OneDrive. Bind-mounting ~/.config/gh from OneDrive is plausible but the file-locking semantics of cloud sync against gh's token file are untested. Need to verify or carve those flavors out.
Security blast radius. A bind mount exposes the entiregh config — every host (gist, enterprise, etc.) and every active token — to whatever runs in the container. That is not worse than the env-var status quo on its own, but it is broader: the env var exposes one token; the mount exposes all of them. Worth naming and documenting; possibly mount ,readonly so the container cannot mutate host auth.
What to do with remoteEnv: GH_TOKEN once the mount works. Three options:
Remove it. Strongest; eliminates the shadowing class entirely. Breaks any user who relies on the env-var path for non-gh tooling.
Keep it but only when the mount is absent. Conditional behavior again — devcontainer JSON doesn't express this cleanly without extra tooling.
Drop the ${localEnv:GH_TOKEN} reference but keep the key wired through to ${env:GH_TOKEN} set inside the container. Lets gh auth token populate it via post-create, breaking the shadowing path while preserving downstream consumers (MCP, gh api callers that read GH_TOKEN).
Interaction with gh auth login from inside the container. With the mount in place, an in-container gh auth login writes to the host's config file. Is that a feature (one login covers both) or a footgun (a container running with ,readonly would fail; without ,readonly, a malicious or buggy container could overwrite host auth state)?
Suggested direction (pending the cross-platform call)
POSIX-first. Ship the mount unconditionally on macOS and Linux. Document the Windows path separately and let Windows users either run from WSL2 (mount works as-is) or follow a documented manual instruction.
Mount as ,readonly. The container reads the host's token but cannot write to it. This blocks the in-container gh auth login footgun and shrinks the security blast radius. gh auth token reads still work.
Stop setting GH_TOKEN via remoteEnv. Remove the line; let gh discover its credential via the mounted config. If a downstream consumer needs the literal env var, set it from gh auth token in post-create.sh so the value comes from the live source.
Document the trade-off. A short section in each affected flavor's docs and in docs/cli.md covering: what the mount exposes, why it replaces GH_TOKEN, the Windows workaround, and the ,readonly choice.
Provisional acceptance criteria
Cross-platform decision documented (where in repo: an ADR or docs/engine/).
Code flavors' devcontainer.json.tmpl updated to use the agreed mechanism (mount and/or env, per the decision).
remoteEnv: GH_TOKEN line removed or replaced per the decision, so an expired host env var cannot shadow a fresh in-container login.
Golden snapshots regenerated; smoke-test green.
Docs updated: docs/cli.md (or a new docs/engine/devcontainer-auth.md), affected flavor docs, and the README if it mentions auth setup.
Windows fallback path (WSL2 vs native) documented and tested at least manually.
Security note: what is exposed to the container, and why ,readonly (if chosen) is correct.
Not building
A bespoke credential broker. The gh config file is the existing source of truth; reuse it.
A devcontainer feature / image change to install gh differently. The current install path is fine; this issue is about how credentials reach the running container.
Anything that mutates the host's gh config from inside the container by default (covered by ,readonly).
This repo's own .devcontainer/devcontainer.json:19 carries the same GH_TOKEN line and exhibits the same shadowing — fixing the templates without fixing the dogfood devcontainer leaves the maintainer hitting the bug they shipped a fix for.
Problem
Every code flavor's devcontainer exposes the host's GitHub credential via:
(see
internal/flavors/gocli/templates/.devcontainer/devcontainer.json.tmpl:19and the equivalent infullstack,gobackend,iac— and in this repo's own.devcontainer/devcontainer.json:19.)In practice this is brittle for agentic work:
GH_TOKENis set in the host shell from an earlier session and expires or rotates, the container inherits the dead token. The token from the keyring (whichgh auth loginpopulated) is ignored becauseGH_TOKENtakes precedence ingh. The agent seesgh auth status: The token in GH_TOKEN is invalid.and every subsequentgh/ GitHub MCP call fails withBad credentials, even thoughgh auth loginwas performed in the container.unset GH_TOKENto getghworking at all. Once unset,gh auth statusreportedActive account: trueagainst the keyring login.GH_TOKENis set,gh repo cloneand friends embed the token in the clonedoriginURL (https://x-access-token:ghp_...@github.com/...). The URL persists after the token is revoked, so futuregit pushfails authentication even though the keyring login works. Cleanup requires the user to know to resetorigin.GH_TOKEN-modeghwill not let the usergh auth loginover it — the env var wins. The user has to leave the container, reset the host env var, and rebuild.ghitself can use either env var or keyring. The GitHub MCP server expects an env var. We are conflating them by routing both throughGH_TOKEN, which is exactly where the stale-shadowing bug bites.The net effect: agents working inside an agent-init devcontainer hit GitHub-auth dead ends often enough that troubleshooting it is a recurring tax.
What seems to work better
Mount the host's
ghconfig directory into the container instead of passing the token as an env var:Effects observed:
ghinside the container uses the same auth state as the host. The keyring login made on the host (gh auth login) is visible to the container immediately. No env var, no shadowing.gh auth refreshon the host updates the same file the container reads.export GITHUB_TOKEN="$(gh auth token)"from the live state, which is the patterndocs/cli.md(theadd-trackersection) already recommends.ghis the auth source, not an env var).Open design questions — the load-bearing ones
${localEnv:HOME}/.config/ghis correct on macOS and Linux. On Windows, the host config path differs:~/.config/ghworks (Linux file system).%USERPROFILE%\AppData\Roaming\GitHub CLI\(perghdocs), which${localEnv:HOME}does not resolve to.${localEnv:USERPROFILE}exists on Windows but the path inside is different again.${localEnv:GH_CONFIG_DIR}variable is set and rely on a host-side helper to set it, (c) skip the mount on Windows and fall back togh auth loginin-container with no env var (the keyring lives in the container, but is wiped on rebuild).claude-cowork,project-management) target users whose home folder may be inside OneDrive. Bind-mounting~/.config/ghfrom OneDrive is plausible but the file-locking semantics of cloud sync againstgh's token file are untested. Need to verify or carve those flavors out.ghconfig — every host (gist, enterprise, etc.) and every active token — to whatever runs in the container. That is not worse than the env-var status quo on its own, but it is broader: the env var exposes one token; the mount exposes all of them. Worth naming and documenting; possibly mount,readonlyso the container cannot mutate host auth.remoteEnv: GH_TOKENonce the mount works. Three options:${localEnv:GH_TOKEN}reference but keep the key wired through to${env:GH_TOKEN}set inside the container. Letsgh auth tokenpopulate it via post-create, breaking the shadowing path while preserving downstream consumers (MCP,gh apicallers that readGH_TOKEN).GITHUB_TOKEN/GITHUB_PERSONAL_ACCESS_TOKENin the MCP client's environment (docs/cli.mdadd-trackersection). With the mount in place, the canonical export line isexport GITHUB_TOKEN="$(gh auth token)". Decide whether this lands as: a one-liner in the post-create script, an exported env in shell rc, or stays a documented manual step. (Related: feat(cli): guided credential setup for non-technical users (tracker MCP on doc-collab flavors) #67, test: verify tracker MCP credential resolution works in Codex #68.)gh auth loginfrom inside the container. With the mount in place, an in-containergh auth loginwrites to the host's config file. Is that a feature (one login covers both) or a footgun (a container running with,readonlywould fail; without,readonly, a malicious or buggy container could overwrite host auth state)?Suggested direction (pending the cross-platform call)
,readonly. The container reads the host's token but cannot write to it. This blocks the in-containergh auth loginfootgun and shrinks the security blast radius.gh auth tokenreads still work.GH_TOKENviaremoteEnv. Remove the line; letghdiscover its credential via the mounted config. If a downstream consumer needs the literal env var, set it fromgh auth tokeninpost-create.shso the value comes from the live source.docs/cli.mdcovering: what the mount exposes, why it replacesGH_TOKEN, the Windows workaround, and the,readonlychoice.Provisional acceptance criteria
docs/engine/).devcontainer.json.tmplupdated to use the agreed mechanism (mount and/or env, per the decision).remoteEnv: GH_TOKENline removed or replaced per the decision, so an expired host env var cannot shadow a fresh in-container login.docs/cli.md(or a newdocs/engine/devcontainer-auth.md), affected flavor docs, and the README if it mentions auth setup.,readonly(if chosen) is correct.Not building
ghconfig file is the existing source of truth; reuse it.ghdifferently. The current install path is fine; this issue is about how credentials reach the running container.ghconfig from inside the container by default (covered by,readonly).Related
claude-cowork(no devcontainer; bind-mount may not apply, which is itself information)..devcontainer/devcontainer.json:19carries the sameGH_TOKENline and exhibits the same shadowing — fixing the templates without fixing the dogfood devcontainer leaves the maintainer hitting the bug they shipped a fix for.