ARD-0044: Frictionless in-container GitHub auth — inject the host gh token by default¶
- Status: Accepted
- Date: 2026-06-23
- Type: Mini-ARD
- Deciders: Tom (Claude facilitating)
- Amends: ARD-0005 — carves a bounded, opt-out exception in the credential-starvation default. ARD-0011 / ARD-0036 — opens
github.com/api.github.comHTTPS in the allowlist when active. - Related: [[ard-0002-dbx-as-runtime-dependency]], [[ard-0022-boring-ui-session-and-trust-model]], [[ard-0032-local-secret-provisioning-into-os-keyring]]
Context¶
A dev container can commit but can't push. The remote is an SSH URL, the runtime image has no ssh binary, and the host gh token lives in a keyring the container can't reach (no dbus). So an agent working inside the container finishes the code, then hands the push + PR back to a human host shell — the recurring friction this ARD removes.
The host-side boring save path (ARD-0022) already pushes and opens PRs using the host's own auth, and remains the safe default for the boring-ui marketer surface. But for an engineer (or a headless agent) working inside the container, that handoff is the whole cost.
Decision¶
At boring open / boring run, if the repo's origin is github.com and a token is available, boring injects it into the container so git push / gh work in-place. The token is sourced, in precedence order:
BORING_GIT_TOKEN(host env escape hatch),keychain:boring-github/github.com(a scoped-PAT override, provisioned viaboring git-auth login),gh auth token— the engineer's existing host login. This is the frictionless default: no provisioning, no per-repo profile field.
Injection rides the same in-memory --remote-env channel as secrets (nothing on disk):
GH_TOKENsoghauthenticates.GIT_CONFIG_*env (no config file) that rewrite the SSH remote → HTTPS (url.insteadOf) and add a token-from-env credential helper. This sidesteps the absentssh/dbus/keyring entirely —git pushworks over HTTPS with the token.user.name/user.emailforwarded from the host so commits are attributable.
When egress is enforced, github.com + api.github.com are appended to the allowlist (the ARD-0036 floor only opens :22/SSH; the token path is HTTPS/443).
Defaults & gates. On by default when a host token exists. Silent no-op when none exists. Never runs for --ui (marketer) opens. Opt out per-repo with git_auth: false in .boring/profile.yaml, or globally with BORING_NO_GIT_AUTH=1. boring git-auth status shows what would be injected.
Rationale¶
The frictionless bar ("auto, no setup") is only met by reusing the auth the engineer already has — gh auth token. Any provision-first scheme (a dedicated PAT, a keyring entry that boring open hard-fails without) reintroduces a setup step and, worse, a fail-fast that breaks boring open for anyone who hasn't done it. Sourcing from gh inverts that: present → it works; absent → nothing changes and the host-side path still covers you.
Configuring git through GIT_CONFIG_* env rather than a container script means no postCreateCommand change, no file written, and the token never lands on the container FS — it lives only in the process env, like every other secret boring injects.
Consequences¶
- Positive: in-container
git push/gh pr createjust work; agents stop handing pushes back to the host; zero per-repo or per-machine setup for the common case; nothing on disk. - Negative / the trade-off: this puts a push-capable GitHub token in every container, reachable by a prompt-injected agent, and opens
github.comegress — a deliberate hole in the ARD-0005 starvation default. The blast radius is exactly the token's own scope. The hostghtoken is typically broad (repo,workflow, oftenadmin:org), so the mitigation is procedural and surfaced everywhere: substitute a fine-grained PAT scoped to just the repos you use boring with viaboring git-auth login(orBORING_GIT_TOKEN), which caps it to "can push to those repos."boring git-auth statusandboring doctorboth report the active source so the exposure is never silent. - Neutral: off for the marketer UI surface (explicit
--uiand profileui.enabled: true); per-repo (git_auth: false, an overlay-protected field) and global (BORING_NO_GIT_AUTH=1) opt-outs exist for anyone who wants the starved default back.
Exposure surfaces (not "in-memory only")¶
The "nothing on disk" framing is about boring's own files (compose YAML, devcontainer.json, audit logs) — verified: the token reaches none of them, and logs name only the source. Two channels still carry the token and are worth naming honestly:
- Host argv. boring injects via the devcontainer CLI's
--remote-env KEY=VALUE, soGH_TOKEN=<token>is visible in host process argv (ps,/proc/<pid>/cmdline) for the duration ofdevcontainer up. This is a pre-existing property of boring's secret channel (everysecret://rides it the same way); ARD-0044 only changes that a token now rides it by default on github.com repos. Tracked for a channel-wide fix (move secrets onto theBORING_EGRESS_MODEcompose-interpolation pattern, off argv) in steig/boring#41 — deferred to its own change because it must be verified against a live container. - Container lifecycle hooks.
--remote-envvalues are present in the env ofpostCreateCommand/setup:hooks, so a profile-authored hook that dumps its environment to a file would landGH_TOKENon the container FS. That is a profile-author footgun (the profile is the trust anchor, ARD-0006), now reachable by default — call it out in profile authoring guidance.
Alternatives considered¶
- Provision a dedicated token into the keyring (ARD-0032 style), referenced from each profile. Rejected as the default: a
secret://ref in a committed profile hard-failsboring openuntil every machine provisions it — the opposite of frictionless. Kept as the override path (precedence #2) for those who want a scoped token. - SSH agent forwarding into the container. Rejected: the runtime image has no
sshbinary, and forwarding the agent socket is more moving parts than an HTTPS credential helper that needs nothing in-container. - Keep host-side
boring saveas the only path. Rejected here because it is the friction being removed — but it stays the default for--uiand the fallback when no token exists.