From 50268c0055a85a6cc3cd2c2d053ed913847115f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 07:06:57 +0000 Subject: [PATCH 1/3] feat(agents)!: authenticate gh via 1Password instead of persisting credentials (v2.0.0) Replace the host-side devcontainer-gh-config volume (which persisted a long-lived GitHub token indefinitely) with on-demand token injection from 1Password: - Drop the devcontainer-gh-config mount and its chown; Claude config volume stays persisted. - Bundle the 1Password CLI (op) in install.sh. - Add a ghTokenSecretRef option; a /etc/profile.d snippet resolves GH_TOKEN from the 1Password secret reference fresh per shell (no host persistence). No-op when unset/unauthenticated, falling back to gh auth login / GITHUB_TOKEN. - Update README and tests; bump feature to v2.0.0 (removes gh persistence). --- README.md | 57 ++++++++++++++++++---- src/agents/devcontainer-feature.json | 16 +++--- src/agents/install.sh | 73 +++++++++++++++++++++++++++- test/agents/test.sh | 2 + 4 files changed, 128 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index eab60e4..11d60c5 100644 --- a/README.md +++ b/README.md @@ -13,18 +13,53 @@ 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 -### Persistent Claude + `gh` logins +### Persistent Claude login -The feature mounts named volumes (`devcontainer-claude-config` at `/home/node/.claude`, -`devcontainer-gh-config` at `/home/node/.config/gh`) and sets `CLAUDE_CONFIG_DIR`, so the -Claude and `gh auth login` sessions survive container **rebuilds** — you log in once -instead of after every rebuild. A `postCreateCommand` `chown`s the volumes so the `node` -user can write to them. +The feature mounts a named volume (`devcontainer-claude-config` at `/home/node/.claude`) +and sets `CLAUDE_CONFIG_DIR`, so the Claude session survives container **rebuilds** — you +log in once instead of after every rebuild. A `postCreateCommand` `chown`s the volume so +the `node` user can write to it. -> The volumes are **shared across all repos** that use this feature (these logins are +> The volume is **shared across all repos** that use this feature (the Claude login is > account-level, not repo-level), so logging in from one container carries over to the -> others. The mount paths assume the `node` remote user (the base image we standardize on). +> others. The mount path assumes the `node` remote user (the base image we standardize on). + +### `gh` auth via 1Password (no host-side credentials) + +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. + +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: + +```jsonc +"features": { + "ghcr.io/rocicorp/devcontainer-features/agents:2": { + "ghTokenSecretRef": "op://Engineering/GitHub CLI/token" + } +}, +// pass the 1Password service-account token through from the host environment +"remoteEnv": { + "OP_SERVICE_ACCOUNT_TOKEN": "${localEnv:OP_SERVICE_ACCOUNT_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. + +[secret-ref]: https://developer.1password.com/docs/cli/secret-references/ +[service-account]: https://developer.1password.com/docs/service-accounts/ ### Usage @@ -32,9 +67,11 @@ In any repo's `.devcontainer/devcontainer.json`: ```jsonc "features": { - "ghcr.io/rocicorp/devcontainer-features/agents:1": { + "ghcr.io/rocicorp/devcontainer-features/agents:2": { // optional — override the pinned Codex version - "codexVersion": "0.139.0" + "codexVersion": "0.139.0", + // optional — 1Password secret reference for the gh token (see above) + "ghTokenSecretRef": "op://Engineering/GitHub CLI/token" } } ``` diff --git a/src/agents/devcontainer-feature.json b/src/agents/devcontainer-feature.json index 3b09924..bc23b2c 100644 --- a/src/agents/devcontainer-feature.json +++ b/src/agents/devcontainer-feature.json @@ -1,14 +1,19 @@ { "id": "agents", - "version": "1.2.0", + "version": "2.0.0", "name": "AI Coding Agents (Codex + Claude Code + GitHub CLI)", - "description": "Installs the OpenAI Codex CLI, Claude Code, and the GitHub CLI so every dev container ships with the agents pre-installed. Persists the Claude and `gh` logins across rebuilds via named volumes.", + "description": "Installs the OpenAI Codex CLI, Claude Code, the GitHub CLI, and the 1Password CLI so every dev container ships with the agents pre-installed. Persists the Claude login across rebuilds via a named volume. Authenticates `gh` by injecting a short-lived token from 1Password at shell start instead of persisting GitHub credentials on the host.", "documentationURL": "https://github.com/rocicorp/devcontainer-features/tree/main/src/agents", "options": { "codexVersion": { "type": "string", "default": "0.139.0", "description": "Version of @openai/codex to install (npm dist-tag or exact version). Pin an exact version for reproducibility." + }, + "ghTokenSecretRef": { + "type": "string", + "default": "", + "description": "1Password secret reference (op://vault/item/field) for a GitHub token. When set, GH_TOKEN is resolved from 1Password fresh at each shell start via `op read`, so no GitHub credentials are persisted on the host. Requires `op` to be authenticated in the container (set OP_SERVICE_ACCOUNT_TOKEN). Leave empty to fall back to `gh auth login` or a GITHUB_TOKEN env var." } }, "dependsOn": { @@ -19,17 +24,12 @@ "CLAUDE_CONFIG_DIR": "/home/node/.claude" }, "mounts": [ - { - "source": "devcontainer-gh-config", - "target": "/home/node/.config/gh", - "type": "volume" - }, { "source": "devcontainer-claude-config", "target": "/home/node/.claude", "type": "volume" } ], - "postCreateCommand": "sudo chown -R node:node /home/node/.config/gh /home/node/.claude 2>/dev/null || true", + "postCreateCommand": "sudo chown -R node:node /home/node/.claude 2>/dev/null || true", "installsAfter": ["ghcr.io/devcontainers/features/node"] } diff --git a/src/agents/install.sh b/src/agents/install.sh index a272442..b07b19b 100755 --- a/src/agents/install.sh +++ b/src/agents/install.sh @@ -3,7 +3,9 @@ set -euo pipefail # Feature options are passed in as uppercased env vars. CODEX_VERSION="${CODEXVERSION:-latest}" +GH_TOKEN_OP_REF="${GHTOKENSECRETREF:-}" +# --- OpenAI Codex CLI --------------------------------------------------------- if ! command -v npm >/dev/null 2>&1; then echo "ERROR: npm is required to install @openai/codex but was not found on PATH." >&2 echo " Add a Node.js feature (or use a node base image) before this feature." >&2 @@ -12,7 +14,74 @@ fi echo "Installing @openai/codex@${CODEX_VERSION} ..." npm install -g "@openai/codex@${CODEX_VERSION}" - -echo "Done. Installed:" codex --version || true # Claude Code is provided by the dependsOn feature (anthropics/claude-code). +# GitHub CLI is provided by the dependsOn feature (devcontainers/github-cli). + +# --- 1Password CLI (op) ------------------------------------------------------- +# Bundled so `gh` can authenticate from a short-lived token resolved out of +# 1Password at shell start, instead of persisting GitHub credentials on the host. +install_op() { + if command -v op >/dev/null 2>&1; then + echo "1Password CLI already installed: $(op --version)" + return 0 + fi + if ! command -v apt-get >/dev/null 2>&1; then + echo "WARNING: apt-get not found; skipping 1Password CLI install." >&2 + echo " Install 'op' manually if you want gh-via-1Password auth." >&2 + return 0 + fi + + local arch keyring + arch="$(dpkg --print-architecture)" + keyring=/usr/share/keyrings/1password-archive-keyring.gpg + export DEBIAN_FRONTEND=noninteractive + + echo "Installing the 1Password CLI ..." + apt-get update + apt-get install -y --no-install-recommends curl gnupg ca-certificates + + curl -sS https://downloads.1password.com/linux/keys/1password.asc \ + | gpg --dearmor --yes --output "$keyring" + echo "deb [arch=${arch} signed-by=${keyring}] https://downloads.1password.com/linux/debian/${arch} stable main" \ + > /etc/apt/sources.list.d/1password.list + + # debsig verification policy (per 1Password's official install docs). + mkdir -p /etc/debsig/policies/AC2D62742012EA22/ + curl -sS https://downloads.1password.com/linux/debian/debsig/1password.pol \ + > /etc/debsig/policies/AC2D62742012EA22/1password.pol + mkdir -p /usr/share/debsig/keyrings/AC2D62742012EA22 + curl -sS https://downloads.1password.com/linux/keys/1password.asc \ + | gpg --dearmor --yes --output /usr/share/debsig/keyrings/AC2D62742012EA22/debsig.gpg + + apt-get update + apt-get install -y 1password-cli + op --version || true +} +install_op + +# --- gh token injection via 1Password ---------------------------------------- +# Drop a login-shell profile script that resolves GH_TOKEN from a 1Password +# secret reference (op://vault/item/field) fresh per shell. Requires `op` to be +# authenticated in the container (set OP_SERVICE_ACCOUNT_TOKEN). When no secret +# reference is configured, or op can't resolve it, this is a no-op and you can +# fall back to `gh auth login` or a GITHUB_TOKEN env var. Nothing is persisted +# on the host. +profile_script=/etc/profile.d/10-gh-op-token.sh +cat > "$profile_script" </dev/null 2>&1 \\ + && [ -z "\${GH_TOKEN:-}" ] && [ -z "\${GITHUB_TOKEN:-}" ]; then + if _gh_tok="\$(op read "\${GH_TOKEN_OP_REF}" 2>/dev/null)"; then + export GH_TOKEN="\${_gh_tok}" + fi + unset _gh_tok +fi +EOF +chmod 0644 "$profile_script" + +echo "Done." diff --git a/test/agents/test.sh b/test/agents/test.sh index 0f86a7f..64d793f 100755 --- a/test/agents/test.sh +++ b/test/agents/test.sh @@ -7,5 +7,7 @@ source dev-container-features-test-lib check "codex is installed" bash -c "codex --version" check "claude is installed" bash -c "claude --version" check "gh is installed" bash -c "gh --version" +check "op is installed" bash -c "op --version" +check "gh-op-token profile script is present" bash -c "test -f /etc/profile.d/10-gh-op-token.sh" reportResults From 451b5088a9952063612eeafef4fd02d0f4fbf68e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 07:18:28 +0000 Subject: [PATCH 2/3] ci: add feature test workflow for the agents feature Runs 'devcontainer features test' on pull requests and pushes so the agents feature (codex/claude/gh/op + gh-token profile script) is verified in CI, with no local tooling required. Pins actions/checkout to a commit SHA per org policy. --- .github/workflows/test.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..fd7238e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +name: Test dev container features + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Install devcontainer CLI + run: npm install -g @devcontainers/cli + + - name: Test agents feature + run: | + devcontainer features test \ + --features agents \ + --base-image mcr.microsoft.com/devcontainers/javascript-node:24 \ + . From 5c3cb1be24db714db2413d12d485d3988d4de919 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 07:20:12 +0000 Subject: [PATCH 3/3] ci: cover all three features (agents, docker, pnpm) via matrix --- .github/workflows/test.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd7238e..167b25e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,15 +13,22 @@ permissions: jobs: test: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + feature: + - agents + - docker + - pnpm steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install devcontainer CLI run: npm install -g @devcontainers/cli - - name: Test agents feature + - name: Test ${{ matrix.feature }} feature run: | devcontainer features test \ - --features agents \ + --features ${{ matrix.feature }} \ --base-image mcr.microsoft.com/devcontainers/javascript-node:24 \ .