// Devcontainer for GitNexus. Pre-installs Claude Code, OpenAI Codex CLI, // and Cursor CLI alongside the Node.js native build chain. Cross-platform // across macOS, Linux, and Windows-via-WSL2 (Windows-native is unsupported // — see .devcontainer/README.md § Windows 11 setup). Opens via the VS Code // Dev Containers extension. // // First-time setup, auth flows, and troubleshooting: .devcontainer/README.md. { "name": "GitNexus AI CLI Devcontainer", "build": { "dockerfile": "Dockerfile", "context": ".", "args": { "CLAUDE_CODE_VERSION": "2.1.153", "CODEX_VERSION": "0.134.0", "CURSOR_VERSION": "latest", "TZ": "${localEnv:TZ:UTC}" } }, // Runs on the HOST (not the container) before container create. The // single-string form is the spec-canonical shape for cross-platform // command-property dispatch; the object form is "named parallel tasks", // not OS dispatch. We use Node so the same command works in cmd.exe on // Windows and bash/zsh on Linux/macOS/WSL — the script reads `os.homedir()` // (which respects $HOME on POSIX and %USERPROFILE% on Windows) and creates // the host-side bind mount sources idempotently. Host prerequisite: Node // on PATH (the only host-side toolchain dependency beyond Docker Desktop // and the VS Code Dev Containers extension). "initializeCommand": "node .devcontainer/ensure-host-config-dirs.cjs", "features": { "ghcr.io/devcontainers/features/github-cli:1": {} }, "remoteUser": "node", "updateRemoteUserUID": true, "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated", "workspaceFolder": "/workspace", // Mount topology, by group: // // 1. AI CLI host config — read-only bind at /host/.. `post-create.sh` // selectively symlinks shareable subdirs (plugins/skills/agents/memory/ // commands) into the container's named-volume config dirs, and copies // credentials + `.claude.json` (the onboarding-state file Claude Code // looks for at $HOME, *outside* ~/.claude/) on first run. Read-only // eliminates write-through from container to host — a compromised npm // dep can't drop an `agents/evil.md` into your host ~/.claude/agents/ // that the next host Claude session would auto-load. The host stays // the source of truth for shared content; install new plugins on the // host and rebuild the container to pick them up. // // 2. AI CLI container config — per-devcontainer named volumes. These are // what CLAUDE_CONFIG_DIR / CODEX_HOME / ~/.cursor actually point at. // Credentials live here with proper Linux perms. Container-managed // state (sessions, history, caches, projects/, settings.json) stays // isolated per devcontainer, so two GitNexus checkouts on the same // host don't corrupt each other's chat history or IDE lock files. // // 3. Other host config — read-write or read-only bind mounts for things // that don't have the perm-flattening / onboarding-state complexity // Claude Code does. `~/.gitconfig` is handled separately by VS Code's // auto-copy mechanism (not mounted). // // 4. Per-instance state — `${devcontainerId}`-scoped: shell history, // npm cache. Survive rebuilds, isolated between sibling instances. // // 5. Per-workspace-name AND per-instance state — workspace `node_modules` // volumes use both `${localWorkspaceFolderBasename}` (debuggable in // `docker volume ls`) and `${devcontainerId}` (collision-free between // sibling instances of the same repo). Keeps tree-sitter native // binaries and onnxruntime off the workspace bind mount (Win/Mac perf). "mounts": [ "source=${localEnv:HOME}/.claude,target=/host/.claude,type=bind,readonly", "source=${localEnv:HOME}/.codex,target=/host/.codex,type=bind,readonly", "source=${localEnv:HOME}/.cursor,target=/host/.cursor,type=bind,readonly", "source=${localEnv:HOME}/.claude.json,target=/host/.claude.json,type=bind,readonly", "source=claude-config-${devcontainerId},target=/home/node/.claude,type=volume", "source=codex-config-${devcontainerId},target=/home/node/.codex,type=volume", "source=cursor-config-${devcontainerId},target=/home/node/.cursor,type=volume", "source=${localEnv:HOME}/.config/git,target=/home/node/.config/git,type=bind,readonly", "source=${localEnv:HOME}/.ssh,target=/home/node/.ssh,type=bind,readonly", "source=${localEnv:HOME}/.config/gh,target=/home/node/.config/gh,type=bind", "source=${localEnv:HOME}/.docker,target=/home/node/.docker,type=bind", "source=${localEnv:HOME}/.aws,target=/home/node/.aws,type=bind,readonly", "source=${localEnv:HOME}/.azure,target=/home/node/.azure,type=bind,readonly", "source=commandhistory-${devcontainerId},target=/commandhistory,type=volume", "source=npm-cache-${devcontainerId},target=/home/node/.npm,type=volume", "source=${localWorkspaceFolderBasename}-root-node-modules-${devcontainerId},target=/workspace/node_modules,type=volume", "source=${localWorkspaceFolderBasename}-gitnexus-node-modules-${devcontainerId},target=/workspace/gitnexus/node_modules,type=volume", "source=${localWorkspaceFolderBasename}-gitnexus-web-node-modules-${devcontainerId},target=/workspace/gitnexus-web/node_modules,type=volume", "source=${localWorkspaceFolderBasename}-gitnexus-shared-node-modules-${devcontainerId},target=/workspace/gitnexus-shared/node_modules,type=volume" ], // Interactive login is the default auth path for all three CLIs; // credentials persist in the host-bind-mounted directories (~/.claude, // ~/.codex, ~/.cursor) declared in the mounts block above. // API keys (ANTHROPIC_API_KEY / OPENAI_API_KEY / CURSOR_API_KEY) are NOT // injected via containerEnv — `${localEnv:VAR}` resolves an unset host var // to an empty string, and Cursor in particular treats `CURSOR_API_KEY=""` // as "use this empty key" rather than "fall back to stored login", which // would silently break `cursor-agent login`. Users who need API key auth // should `export` the var in their container shell or carry it via their // VS Code dotfiles repo (see .devcontainer/README.md). // CLAUDE_CONFIG_DIR is intentionally NOT set. The Claude default is // `$HOME/.claude` (= `/home/node/.claude`), which is exactly where the // claude-config named volume mounts — setting the env var explicitly // changes which file Claude reads `hasCompletedOnboarding` from: with // the var set, Claude reads `$CLAUDE_CONFIG_DIR/.claude.json` (the // small identity file); without it, Claude reads `$HOME/.claude.json` // (the big onboarding-state file that actually carries // `hasCompletedOnboarding`, MCP user-scope config, and per-project // trust). Leaving the var unset matches host behavior and lets // post-create.sh's host-sync of `$HOME/.claude.json` skip the wizard // on every container-create. // // CODEX_HOME is kept (even though it matches the Codex default) as a // canary: if we ever move the Codex named volume target, the env var // makes the dependency explicit instead of silently following the // default. "containerEnv": { "CODEX_HOME": "/home/node/.codex", "DISABLE_AUTOUPDATER": "1", "HISTFILE": "/commandhistory/.zsh_history" }, "customizations": { "vscode": { "extensions": [ "anthropic.claude-code", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "eamodio.gitlens" ], "settings": { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "files.eol": "\n", "terminal.integrated.defaultProfile.linux": "zsh", "terminal.integrated.profiles.linux": { "bash": { "path": "bash", "icon": "terminal-bash" }, "zsh": { "path": "zsh" } } } } }, // 4747 (gitnexus serve) must not be remapped: gitnexus-web hardcodes // http://localhost:4747 as the default backend URL. "forwardPorts": [5173, 4747, 4173], "portsAttributes": { "5173": { "label": "Vite dev (gitnexus-web)", "onAutoForward": "notify" }, "4747": { "label": "gitnexus serve HTTP API", "onAutoForward": "notify", "requireLocalPort": true }, "4173": { "label": "Static web (Vite preview)", "onAutoForward": "silent" } }, // Driver script with labeled steps lives at .devcontainer/post-create.sh // so each step's success/failure is visible in the log without parsing // an &&-chain. Run via `bash` explicitly so the script doesn't depend // on its executable bit surviving the workspace bind mount. "postCreateCommand": "bash .devcontainer/post-create.sh" }