{
  "markdown": "# devcontainer-feature-ai-assistant\n\nA collection of [dev container Features](https://containers.dev/implementors/features/) that install AI coding assistant CLIs into your dev container.\n\n| Feature | CLI installed | Published at |\n| ------- | ------------- | ------------ |\n| [`claude`](src/claude/README.md) | [Claude Code](https://claude.ai/) | `ghcr.io/e0ipso/devcontainer-feature-ai-assistant/claude:1` |\n| [`codex`](src/codex/README.md) | [OpenAI Codex CLI](https://github.com/openai/codex) | `ghcr.io/e0ipso/devcontainer-feature-ai-assistant/codex:1` |\n| [`copilot`](src/copilot/README.md) | [GitHub Copilot CLI](https://github.com/github/copilot-cli) | `ghcr.io/e0ipso/devcontainer-feature-ai-assistant/copilot:1` |\n| [`cursor`](src/cursor/README.md) | [Cursor CLI](https://cursor.com/) (`agent`) | `ghcr.io/e0ipso/devcontainer-feature-ai-assistant/cursor:1` |\n| [`opencode`](src/opencode/README.md) | [OpenCode](https://opencode.ai/) | `ghcr.io/e0ipso/devcontainer-feature-ai-assistant/opencode:1` |\n| [`t3`](src/t3/README.md) | [T3 Code](https://github.com/pingdotgg/t3code) | `ghcr.io/e0ipso/devcontainer-feature-ai-assistant/t3:1` |\n\nPlus four opt-in companions that forward host credentials into the container. Each depends on its CLI feature, so listing the `*-auth` feature alone installs both:\n\n| Feature | Forwards | Published at |\n| ------- | -------- | ------------ |\n| [`claude-auth`](src/claude-auth/README.md) | `~/.claude/.credentials.json` | `ghcr.io/e0ipso/devcontainer-feature-ai-assistant/claude-auth:1` |\n| [`codex-auth`](src/codex-auth/README.md) | `~/.codex/auth.json` | `ghcr.io/e0ipso/devcontainer-feature-ai-assistant/codex-auth:1` |\n| [`cursor-auth`](src/cursor-auth/README.md) | `~/.config/cursor/auth.json` | `ghcr.io/e0ipso/devcontainer-feature-ai-assistant/cursor-auth:1` |\n| [`opencode-auth`](src/opencode-auth/README.md) | `~/.local/share/opencode/auth.json` | `ghcr.io/e0ipso/devcontainer-feature-ai-assistant/opencode-auth:1` |\n\nThere is no `t3-auth` (t3 keeps its state on a named volume, which survives rebuilds) and no `copilot-auth` (Copilot authenticates through `gh` and has no credential file).\n\n---\n\n## Using a feature in your project\n\nAdd a `features` block to your project's `.devcontainer/devcontainer.json`:\n\n```jsonc\n{\n  \"image\": \"mcr.microsoft.com/devcontainers/base:ubuntu\",\n  \"features\": {\n    // Pick one or more:\n    \"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/claude:1\": {},\n    \"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/codex:1\": {},\n    \"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/copilot:1\": {},\n    \"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/cursor:1\": {},\n    \"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/opencode:1\": {},\n    \"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/t3:1\": {}\n\n    // To also forward your host credentials, use the matching *-auth feature\n    // instead — it installs the CLI too. See \"Forwarding host authentication\".\n    // \"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/claude-auth:1\": {}\n  }\n}\n```\n\n> **Node requirement.** The `codex`, `copilot`, `opencode`, and `t3` features install via npm and require Node.js in the base image (or the `ghcr.io/devcontainers/features/node` feature installed before them). The `claude` and `cursor` features use shell installers that also declare `installsAfter: node` — use a Node base image or add the Node feature when composing with other features.\n\n### Recommended base image\n\n`node:24` or `mcr.microsoft.com/devcontainers/universal:2` cover all features out of the box.\n\n---\n\n## Forwarding host authentication\n\nUse the `*-auth` features. Add one entry and you get the CLI, the host mount, and the seeding script together:\n\n```jsonc\n{\n  \"image\": \"node:24\",\n  \"features\": {\n    \"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/claude-auth:1\": {}\n  }\n}\n```\n\nOn first start the feature copies the host credential file into place — only when the destination does not already exist — and `chmod 600`s it. Container-side credentials drift freely afterwards, and a host with nothing to forward is a no-op.\n\n### Why this is a separate feature\n\nA mount has exactly one on/off switch: whether the feature appears in your `features` list. Feature options are delivered to `install.sh` as `_BUILD_ARG_*` environment variables, and no option-based substitution variable exists, so a static `mounts` block can never see an option. Putting the mount in `claude:1` would make host-home forwarding mandatory and undeclinable; the feature therefore *is* the unit of opt-in.\n\nKeeping the mount and the script in one artifact is the other half of the reasoning. When the mount lived in your `devcontainer.json` and the script lived in the CLI feature, the two could version-skew — and the stale side can never warn you, because it does not contain the code that would warn. The result was a silent logout.\n\n### Prerequisite: the host directory must exist\n\nEach `*-auth` feature bind-mounts a host *directory* (`~/.claude`, `~/.codex`, `~/.config/cursor`, `~/.local/share/opencode`) at `/cred-seed/<tool>`. The dev containers CLI passes mounts to Docker as `--mount`, which **refuses a missing bind source** instead of creating it the way `-v` does:\n\n```text\ndocker: Error response from daemon: invalid mount config for type \"bind\":\nbind source path does not exist: /home/you/.claude\n```\n\nSo a contributor who has never run the tool on their host cannot start the container until they sign in on the host or run `mkdir -p ~/.claude`. That is loud rather than silent, but it is a real prerequisite for everyone sharing the project, and there is no fallback path — without the directory the container does not start.\n\nThe *directory* is mounted rather than the credential file because the directory exists for anyone who has used the tool at all, while the credential file may legitimately be absent; and because a runtime that does create missing bind sources creates them as directories, which on top of a credential file would break the host install.\n\n### Windows: `HOME` may be unset\n\n`${localEnv:HOME}` resolves against the environment of the process running the CLI, and **a missing variable substitutes to the empty string, not to a literal placeholder**. Under Windows `cmd`/PowerShell `HOME` is normally unset (Windows uses `USERPROFILE`), so `${localEnv:HOME}/.claude` becomes `/.claude` — a root-level path. Git Bash does set `HOME`. The two-argument default form `${localEnv:VAR:default}` exists, but no single expression covers HOME-or-USERPROFILE, so override the mount instead.\n\n### Overriding a feature's mount\n\nA feature mount can be re-pointed but never removed. Mounts are deduplicated by `target`, keeping the **last** occurrence, and a `devcontainer.json`'s own `mounts` are appended after the features', so declaring the same target wins:\n\n```jsonc\n\"mounts\": [\n  \"source=${localEnv:USERPROFILE}/.claude,target=/cred-seed/claude,readonly,type=bind,consistency=cached\"\n]\n```\n\nUse this to fix an unset `HOME`, to point at a different host directory, or to add `readonly` — feature-declared mounts are objects with only `type`, `source` and `target`, so they cannot express `readonly` themselves and the host directory is mounted read-write. To turn forwarding off, drop the feature.\n\n### Lock files pin the old behaviour\n\nIf your project has a `.devcontainer/devcontainer-lock.json`, it pins exact feature versions, and **no change on this side can override it**. After adding or changing an auth feature, run `devcontainer upgrade` (or delete the pinned entries) — otherwise you silently keep whatever the lock file recorded, with no error and no warning. This is exactly how the original credential-forwarding outage happened: a lock file pinned CLI feature versions from before credential seeding shipped, so the consumer's mounts were correct and nothing read them.\n\nConfig seeding (`seedConfig`) and other mounts are documented per feature: [`claude`](src/claude/README.md), [`codex`](src/codex/README.md), [`cursor`](src/cursor/README.md), [`opencode`](src/opencode/README.md), [`t3`](src/t3/README.md).\n\n---\n\n## Feature details\n\n### `claude` — Claude Code CLI\n\nInstalls the [Claude Code](https://claude.ai/) CLI (`claude`) via the official shell installer. The binary lands on a shared, user-writable npm prefix (`/usr/local/share/npm-global/bin`) so it is available to the non-root remote user at runtime.\n\n```jsonc\n\"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/claude:1\": {}\n```\n\nAfter the container starts, run `claude` to authenticate interactively — or add [`claude-auth`](src/claude-auth/README.md) to forward the credentials you already have on the host.\n\n| Option | Type | Default | Description |\n| ------ | ---- | ------- | ----------- |\n| `updateOnPostStart` | `boolean` | `true` | Re-run the Claude Code installer during `postStart` to update to the latest version. |\n| `seedConfig` | `boolean` | `true` | Seed `~/.claude/settings.json` from a host mount or image-baked default on first start only. |\n\n---\n\n### `codex` — OpenAI Codex CLI\n\nInstalls the [Codex CLI](https://github.com/openai/codex) (`codex`) via `npm install -g @openai/codex`. Requires Node.js in the base image.\n\n```jsonc\n\"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/codex:1\": {}\n```\n\nSet `OPENAI_API_KEY` in `remoteEnv`, or run `codex` interactively after the container starts to sign in — or add [`codex-auth`](src/codex-auth/README.md) to forward the credentials you already have on the host.\n\n| Option | Type | Default | Description |\n| ------ | ---- | ------- | ----------- |\n| `updateOnPostStart` | `boolean` | `true` | Run `npm install -g @openai/codex` during `postStart` to update to the latest version. |\n| `seedConfig` | `boolean` | `true` | Seed `~/.codex/config.toml` from a host mount or image-baked default on first start only. |\n\n---\n\n### `copilot` — GitHub Copilot CLI\n\nInstalls the [`@github/copilot`](https://www.npmjs.com/package/@github/copilot) npm package globally.\n\n```jsonc\n\"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/copilot:1\": {}\n```\n\nRequires Node.js in the base image. Authenticate with GitHub (`gh auth login` on the host, or inside the container). Optionally bind-mount `~/.config/gh` to reuse host credentials.\n\n| Option | Type | Default | Description |\n| ------ | ---- | ------- | ----------- |\n| `updateOnPostStart` | `boolean` | `true` | Run `npm install -g @github/copilot` during `postStart` to update to the latest version. |\n\n---\n\n### `cursor` — Cursor CLI\n\nInstalls the [Cursor](https://cursor.com/) agentic CLI via the official shell installer. The installed command is `agent`, not `cursor`.\n\n```jsonc\n\"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/cursor:1\": {}\n```\n\nAfter the container starts, run `agent login` to authenticate interactively, or set `CURSOR_API_KEY` in `remoteEnv` — or add [`cursor-auth`](src/cursor-auth/README.md) to forward the credentials you already have on the host.\n\n| Option | Type | Default | Description |\n| ------ | ---- | ------- | ----------- |\n| `updateOnPostStart` | `boolean` | `true` | Re-run the Cursor installer during `postStart` to update to the latest version. |\n| `seedConfig` | `boolean` | `true` | Seed `~/.cursor/cli-config.json` from a host mount or image-baked default on first start only. |\n\n---\n\n### `opencode` — OpenCode CLI\n\nInstalls [OpenCode](https://opencode.ai/) (`opencode`) via `npm install -g opencode-ai`. Requires Node.js in the base image.\n\n```jsonc\n\"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/opencode:1\": {}\n```\n\nAfter the container starts, run `opencode` to authenticate interactively — or add [`opencode-auth`](src/opencode-auth/README.md) to forward the credentials you already have on the host.\n\n| Option | Type | Default | Description |\n| ------ | ---- | ------- | ----------- |\n| `updateOnPostStart` | `boolean` | `true` | Run `npm install -g opencode-ai` during `postStart` to update to the latest version. |\n| `seedConfig` | `boolean` | `true` | Seed `~/.config/opencode/opencode.json` from a host mount or image-baked default on first start only. |\n\n---\n\n### `t3` — t3 CLI\n\nInstalls the [T3 Code](https://github.com/pingdotgg/t3code) CLI globally via npm and writes a pre-seeded `settings.json` to `/usr/local/share/devcontainer-feature-ai-assistant/t3/settings.json` at build time. On `postStart`, seeds `$T3CODE_HOME/userdata/settings.json` when absent (host mount or the baked default). Requires Node.js in the base image.\n\n```jsonc\n\"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/t3:1\": {\n  \"version\": \"latest\",\n  \"textGenerationModelSelectionInstanceId\": \"claudeAgent\",\n  \"textGenerationModelSelectionModel\": \"claude-sonnet-4-6\",\n  \"enableAssistantStreaming\": true,\n  \"providersClaudeAgentEnabled\": true,\n  \"providersCodexEnabled\": false\n}\n```\n\nSee [`src/t3/devcontainer-feature.json`](src/t3/devcontainer-feature.json) for the full list of options and their defaults.\n\n---\n\n### `claude-auth`, `codex-auth`, `cursor-auth`, `opencode-auth` — host credential forwarding\n\nEach forwards one tool's host credentials into the container, shipping the bind mount and the seeding script as a single versioned artifact. Each declares `dependsOn` on its CLI feature, so one entry installs both.\n\n```jsonc\n\"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/claude-auth:1\": {}\n```\n\n| Feature | Host source (must exist) | Mount target | Live destination |\n| ------- | ------------------------ | ------------ | ---------------- |\n| `claude-auth` | `~/.claude` | `/cred-seed/claude` | `$HOME/.claude/.credentials.json` |\n| `codex-auth` | `~/.codex` | `/cred-seed/codex` | `$HOME/.codex/auth.json` |\n| `cursor-auth` | `~/.config/cursor` | `/cred-seed/cursor` | `$HOME/.config/cursor/auth.json` |\n| `opencode-auth` | `~/.local/share/opencode` | `/cred-seed/opencode` | `$HOME/.local/share/opencode/auth.json` |\n\nNo options — the feature's presence in `features` is the only switch a mount can have. See [Forwarding host authentication](#forwarding-host-authentication) for the prerequisites and caveats, and the per-feature READMEs for the full reasoning: [`claude-auth`](src/claude-auth/README.md), [`codex-auth`](src/codex-auth/README.md), [`cursor-auth`](src/cursor-auth/README.md), [`opencode-auth`](src/opencode-auth/README.md).\n\n---\n\n## Using multiple features together\n\nFeatures are independent and compose freely. A typical full-stack AI setup:\n\n```jsonc\n{\n  \"image\": \"node:24\",\n  \"features\": {\n    \"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/claude:1\": {},\n    \"ghcr.io/e0ipso/devcontainer-feature-ai-assistant/t3:1\": {\n      \"providersClaudeAgentEnabled\": true,\n      \"textGenerationModelSelectionInstanceId\": \"claudeAgent\"\n    }\n  }\n}\n```\n\nEach feature prepends `/usr/local/share/npm-global/bin` to `PATH` via `containerEnv`, so extra `remoteEnv` wiring is usually unnecessary.\n\n---\n\n## Publishing this repo\n\n### One-time setup\n\n1. Fork or push this repo to GitHub.\n2. In **Settings → Actions → General**, enable **Read and write permissions** for the `GITHUB_TOKEN` (the release workflow needs `packages: write` and `contents: write`).\n3. If the release workflow cannot set package visibility via the API, open each package under **Packages**, go to **Package settings**, and set visibility to **Public** (required for unauthenticated `devcontainer` pulls).\n\n### Publish features\n\nAfter CI passes on `main`, the **CI** workflow publishes automatically. You can also run it manually from the Actions tab. It will:\n\n1. Push each `src/<feature>/` directory as an OCI image to `ghcr.io/e0ipso/devcontainer-feature-ai-assistant/<feature>`.\n2. Attempt to set each GHCR package visibility to **public**, then verify anonymous pulls succeed.\n\nFeature READMEs in `src/<feature>/README.md` are maintained by hand and are **not** overwritten by the release workflow.\n\nUntil the release workflow has run at least once, `ghcr.io/e0ipso/devcontainer-feature-ai-assistant/<feature>:1` references will fail with \"Could not resolve Feature manifest\".\n\n### Versioning\n\nEach feature is versioned independently by [semantic-release](https://semantic-release.gitbook.io/), driven by [Conventional Commits](https://www.conventionalcommits.org/) touching that feature's `src/<feature>/` directory:\n\n- `fix:` → patch release\n- `feat:` → minor release\n- `BREAKING CHANGE:` in the commit body (or `!` after the type/scope) → major release\n- other prefixes (`chore:`, `docs:`, `ci:`, `test:`, …) → no release\n\nThe **CI** workflow's `release` job runs `semantic-release` once per feature on every push to `main`. For each feature with a releasable commit since its last tag, it:\n\n1. Writes the new version into `src/<feature>/devcontainer-feature.json`.\n2. Commits that change and pushes a `feature_<id>_<version>` tag.\n3. Publishes a GitHub Release with generated notes.\n\n`\"version\"` in `devcontainer-feature.json` should not be bumped by hand — semantic-release owns that field. It only reads commits scoped to a feature's own directory, so a commit that only touches `src/codex/` will never version-bump `src/cursor/`.\n\nOnce versions are updated, the existing publish step pushes each `src/<feature>/` directory as an OCI image to `ghcr.io/e0ipso/devcontainer-feature-ai-assistant/<feature>` and tags the major version (`:1`) so pinned consumers pick up the update automatically.\n\n---\n\n## Local development & testing\n\n### Prerequisites\n\n```bash\nnpm install -g @devcontainers/cli\n```\n\nThe `*-auth` features bind-mount host credential directories, and Docker refuses a bind mount whose source is missing. Create them once before running their tests, or those containers will not start:\n\n```bash\nmkdir -p ~/.claude ~/.codex ~/.config/cursor ~/.local/share/opencode\n```\n\nCI does the same thing for the same reason (see `.github/workflows/ci.yaml`). Each `*-auth` feature's `seed_credentials` scenario redirects `/cred-seed/<tool>` to a throwaway volume, so tests never write to your real credential directories.\n\n### Run all tests\n\n```bash\n# All features, all scenarios\ndevcontainer features test .\n\n# One feature only\ndevcontainer features test -f claude .\n\n# Specific base image\ndevcontainer features test -f t3 -i node:24 .\n```\n\n### Validate feature JSON\n\n```bash\ndevcontainer features package -f -o /tmp/devcontainer-feature-package ./src\n```\n\n### Project structure\n\n```\nsrc/\n  <feature>/\n    devcontainer-feature.json   # metadata & options schema\n    install.sh                  # runs as root during image build\n    README.md                   # hand-maintained feature docs\ntest/\n  <feature>/\n    test.sh                     # default option smoke test\n    scenarios.json              # named scenario matrix\n  _global/\n    scenarios.json              # multi-feature integration tests\n.github/\n  workflows/\n    ci.yaml                     # validate, test, semantic-release, and publish to GHCR on main\n  scripts/\n    list-feature-ids.sh         # discover src/* feature ids\n    release-features.sh         # run semantic-release once per feature, in order\n    set-feature-version.mjs     # format-preserving version writer used by release.config.mjs\n    post-publish-ghcr.sh        # set GHCR visibility public and verify pulls\n  semantic-release/\n    feature-scope.mjs           # scopes commit-analyzer/release-notes-generator to one feature's directory\nrelease.config.mjs              # shared semantic-release config, parameterized by $FEATURE_ID\npackage.json                    # devDependencies for semantic-release tooling (not a published package)\n```\n",
  "bytes": 19441,
  "sha": "24195ff84e90f5154744490529e89e028d9baf73e0c64cb41fef7512bed2a57d",
  "repo_slug": "e0ipso/devcontainer-feature-ai-assistant",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/okf_e0ipso_devcontainer_feature_ai_assistant_28a20171/readme"
}