Skip to content

devcontainer: GH_TOKEN remoteEnv is unreliable; explore mounting ~/.config/gh into the container #75

Description

@Lillevang

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:

"remoteEnv": {
  ...
  "GH_TOKEN": "${localEnv:GH_TOKEN}"
}

(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:

"mounts": [
  "source=${localEnv:HOME}/.config/gh,target=/home/vscode/.config/gh,type=bind"
]

Effects observed:

  • 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

  1. 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).
  2. 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.
  3. Security blast radius. A bind mount exposes the entire gh 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.
  4. 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).
  5. MCP credential delivery. The GitHub tracker integration expects GITHUB_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN in the MCP client's environment (docs/cli.md add-tracker section). With the mount in place, the canonical export line is export 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.)
  6. 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)

  1. 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.
  2. 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.
  3. 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.
  4. 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).

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:bootstrapInit/bootstrap flow and onboarding UXarea:securitySecrets, credentials, sandbox posturearea:trackersTracker MCP integrations (GitHub/ADO/Jira)enhancementNew feature or requestneeds-refinementDesign/feature needs further technical refinement before implementation

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions