Skip to content

rocicorp/devcontainer-features

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Rocicorp dev container features

Shared dev container Features so every repo's containers come with the same baseline — without copy-pasting post-create.sh across repos.

agents

Installs the AI coding agents we standardize on:

  • OpenAI Codex CLI (@openai/codex, version pinned via the codexVersion option)
  • Claude Code (pulled in automatically via dependsOn on the official 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) — so gh can authenticate from a 1Password-sourced token instead of persisting credentials on the host

Persistent Claude login

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 chowns the volume so the node user can write to it.

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 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. 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.

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.

"features": {
  "ghcr.io/rocicorp/devcontainer-features/agents:2": { "codexVersion": "0.139.0" }
},
// gh reads GITHUB_TOKEN; forward it from the host (resolved there via 1Password)
"remoteEnv": { "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}" }

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 https://1password.com/downloads/command-line, 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):

    export GITHUB_TOKEN="$(op read 'op://Employee/GitHub Personal Access Token/token')"

    Use the item's exact secret reference — 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):
      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 persistentlaunchctl 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 version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
      <key>Label</key><string>com.rocicorp.github-token</string>
      <key>ProgramArguments</key>
      <array>
        <string>/bin/sh</string>
        <string>-c</string>
        <string>launchctl setenv GITHUB_TOKEN "$(/usr/local/bin/op read 'op://Employee/GitHub Personal Access Token/token')"</string>
      </array>
      <key>RunAtLoad</key><true/>
    </dict>
    </plist>

    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.

Usage

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"
  }
}

For gh authentication, add the remoteEnv GITHUB_TOKEN passthrough from gh auth via 1Password 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.

pnpm

Sets up pnpm via Corepack:

  • Runs corepack enable (adds the pnpm shim) at build time.
  • At postCreate, runs corepack install to pin the pnpm version from the workspace's package.json packageManager field.
  • Removes the npm/npx binaries (after build-time installs have run) to enforce pnpm-only usage. This is the default; set removeNpm: false to keep npm available.
"features": {
  "ghcr.io/rocicorp/devcontainer-features/pnpm:1": {}
}

This replaces the corepack/pnpm/npm-removal block that otherwise lives in each repo's post-create.sh. Combined with agents, a consumer repo's devcontainer.json needs no lifecycle scripts at all.

docker

Gives the container a working Docker daemon so tooling that shells out to Docker — most notably testcontainers (used by the zero-cache Postgres integration tests) — runs inside the dev container.

  • Pulls in the official ghcr.io/devcontainers/features/docker-in-docker feature via dependsOn, which installs the Docker engine, runs a daemon inside the container, and adds the remote user to the docker group (no sudo needed).
  • Pins "moby": false so the upstream feature installs Docker CE from Docker's own apt repo instead of Microsoft's moby-* packages, which don't exist on Debian trixie (the base of current javascript-node images) and fail the build.
  • Uses Docker-in-Docker rather than docker-outside-of-docker on purpose: testcontainers relies on bind mounts and container-to-container networking, both of which break under the host-socket approach (path translation) and aren't available in every environment (Codespaces, CI). A self-contained daemon "just works" everywhere.
"features": {
  "ghcr.io/rocicorp/devcontainer-features/docker:1": {}
}

This replaces a per-repo docker-in-docker feature line and centralizes the pinned version alongside the other rocicorp features.

Updating the feature versions everywhere

  1. Bump codexVersion default (and/or the dependsOn claude-code pin) in src/agents/devcontainer-feature.json, raise the feature version, merge to main. The release workflow publishes a new tag to ghcr.io.
  2. Consumer repos pick it up on next rebuild. To avoid hand-editing pins, enable Dependabot (devcontainers ecosystem) in each consumer repo — it opens PRs that bump the devcontainer-lock.json digests automatically.

Publishing

.github/workflows/release.yml publishes all features under src/ to ghcr.io/<owner>/devcontainer-features/<id> on push to main (via devcontainers/action).

After the first publish, make the package public in the repo's Packages settings (or org package visibility) so consumer repos can pull it without auth.

Testing locally

npm install -g @devcontainers/cli
devcontainer features test \
  --features agents \
  --base-image mcr.microsoft.com/devcontainers/javascript-node:24 \
  .

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages