diff --git a/README.md b/README.md index 11d60c5..7103b43 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ Installs the AI coding agents we standardize on: `ghcr.io/anthropics/devcontainer-features/claude-code` feature) - **GitHub CLI** (`gh`, via `dependsOn` on the official `ghcr.io/devcontainers/features/github-cli` feature) -- **1Password CLI** (`op`, installed from 1Password's apt repo) — used to inject a - GitHub token for `gh` without persisting credentials on the host +- **1Password CLI** (`op`, installed from 1Password's apt repo) — so `gh` can authenticate + from a 1Password-sourced token instead of persisting credentials on the host ### Persistent Claude login @@ -31,35 +31,94 @@ the `node` user can write to it. Earlier versions persisted the `gh` login in a `devcontainer-gh-config` volume, which left a long-lived GitHub token sitting on the host indefinitely. As of **v2.0.0** that volume is -gone. Instead, `gh` reads `GH_TOKEN` from a token resolved out of 1Password fresh at each -shell start — nothing is written to the host. +gone. `gh` authenticates from a `GITHUB_TOKEN` **environment variable** instead, resolved +from 1Password on your host and forwarded into the container — nothing is written to the +host. -Set the `ghTokenSecretRef` option to a [1Password secret reference][secret-ref] -(`op://vault/item/field`) pointing at a GitHub token, and provide an -[`OP_SERVICE_ACCOUNT_TOKEN`][service-account] so `op` can authenticate headlessly inside -the container: +The reason it's forwarded from the host rather than resolved inside the container: +**1Password's desktop-app integration (Touch ID unlock) does _not_ work inside a +container.** The `op` CLI's app integration talks to the desktop app over a host-only socket +the container can't reach. So `op` runs on your **host**, and only the resulting token +crosses into the container — `gh` reads `GITHUB_TOKEN` directly. ```jsonc "features": { - "ghcr.io/rocicorp/devcontainer-features/agents:2": { - "ghTokenSecretRef": "op://Engineering/GitHub CLI/token" - } + "ghcr.io/rocicorp/devcontainer-features/agents:2": { "codexVersion": "0.139.0" } }, -// pass the 1Password service-account token through from the host environment -"remoteEnv": { - "OP_SERVICE_ACCOUNT_TOKEN": "${localEnv:OP_SERVICE_ACCOUNT_TOKEN}" -} +// gh reads GITHUB_TOKEN; forward it from the host (resolved there via 1Password) +"remoteEnv": { "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}" } ``` -On each (login) shell, a `/etc/profile.d/10-gh-op-token.sh` snippet runs -`op read "$ghTokenSecretRef"` and exports the result as `GH_TOKEN`, which `gh` uses -automatically. If `ghTokenSecretRef` is empty, `op` is unauthenticated, or the reference -can't be resolved, the snippet is a no-op and you can fall back to `gh auth login` or set -`GITHUB_TOKEN` yourself. Use a token with a short expiry / least-privilege scope so the -injected credential is genuinely short-lived. +One-time **host** setup (the part that tripped us up — note the gotchas): + +1. **Install the `op` CLI** — no package manager required. Download the macOS package or + the standalone universal binary from , + then enable **1Password app → Settings → Developer → Integrate with 1Password CLI** + (Touch ID). Verify with `op vault list`. +2. **Export the token from your shell rc** — for zsh this is **`~/.zshrc`** (not `~/.zsh_rc`, + which zsh never sources): + ```bash + export GITHUB_TOKEN="$(op read 'op://Employee/GitHub Personal Access Token/token')" + ``` + Use the item's exact [secret reference][secret-ref] — in the 1Password app, right-click + the field → **Copy Secret Reference**. Reload and check: `source ~/.zshrc` then + `echo ${#GITHUB_TOKEN}` should be non-zero. +3. **Make the variable visible to the editor process.** `${localEnv:...}` is read from the + editor's *process* environment, and **GUI/Dock/Spotlight launches do _not_ read + `~/.zshrc`** — so a Dock-launched editor won't see `GITHUB_TOKEN`. Two ways to fix it: + + - **Launch from a terminal** (scopes the variable to that editor instance). Fully quit + the editor first, then from a terminal where `echo ${#GITHUB_TOKEN}` is non-zero start + it (`code`). Best when you open a folder/workspace from the CLI. + - **`launchctl setenv`** (works with Dock/Spotlight launches — and with the no-checkout + "Clone Repository in Container Volume" flow, where you never open a folder from the + CLI): + ```bash + launchctl setenv GITHUB_TOKEN "$(op read 'op://Employee/GitHub Personal Access Token/token')" + ``` + This puts the variable into your **GUI login session**, so anything launched afterward + (including a Dock-launched editor) inherits it. Caveats: + - **Relaunch** any already-running editor — it only picks up the value on a fresh launch. + - **Not persistent** — `launchctl setenv` is cleared on logout/restart; re-run it each + session, or automate it with a login LaunchAgent (below). + - **Session-wide** — it's readable by *all* GUI apps in your login session, not just the + editor. If you'd rather keep it scoped, use the terminal launch instead. + + Then build/reopen the container and check `gh auth status`. + +
Optional: re-apply it automatically at login (LaunchAgent) + + Create `~/Library/LaunchAgents/com.rocicorp.github-token.plist`: + + ```xml + + + + + Labelcom.rocicorp.github-token + ProgramArguments + + /bin/sh + -c + launchctl setenv GITHUB_TOKEN "$(/usr/local/bin/op read 'op://Employee/GitHub Personal Access Token/token')" + + RunAtLoad + + + ``` + + Load it with `launchctl load ~/Library/LaunchAgents/com.rocicorp.github-token.plist`. + Caveat: it runs `op read` non-interactively at login, which only succeeds if 1Password can + authorize without a prompt (e.g. the app is unlocked / CLI integration allows it) — + otherwise just run the `launchctl setenv` line by hand each session. +
+ +> Why this shape: only a short-lived *GitHub* token ever enters the container (not a +> credential that can read your whole vault), the secret still originates in 1Password, and +> nothing is persisted on a host volume. If `GITHUB_TOKEN` isn't set, `gh` is simply +> unauthenticated — a clean fallback; run `gh auth login` manually if you like. [secret-ref]: https://developer.1password.com/docs/cli/secret-references/ -[service-account]: https://developer.1password.com/docs/service-accounts/ ### Usage @@ -69,13 +128,14 @@ In any repo's `.devcontainer/devcontainer.json`: "features": { "ghcr.io/rocicorp/devcontainer-features/agents:2": { // optional — override the pinned Codex version - "codexVersion": "0.139.0", - // optional — 1Password secret reference for the gh token (see above) - "ghTokenSecretRef": "op://Engineering/GitHub CLI/token" + "codexVersion": "0.139.0" } } ``` +For `gh` authentication, add the `remoteEnv` `GITHUB_TOKEN` passthrough from +[`gh` auth via 1Password](#gh-auth-via-1password-no-host-side-credentials) above. + This single line replaces the official `claude-code` and `github-cli` feature lines, the inline `npm install -g @openai/codex`, **and** the `.claude` volume / `CLAUDE_CONFIG_DIR` / `chown` wiring that otherwise lives in each repo's `devcontainer.json` + `post-create.sh`.