GitNexus/.devcontainer/devcontainer.json
Gergo Magyar f592c804ed fix(devcontainer): Node-based initializeCommand; bind-mount .ssh + .config/git
Two fixes bundled:

1. The previous commit's OS-keyed `initializeCommand` object was based
   on a misread of the Dev Containers spec. The object form on command
   properties is **named parallel tasks**, not OS dispatch — VS Code ran
   all three keys in parallel via cmd.exe on Windows, the POSIX branches
   failed, and container creation aborted before Docker was invoked.

   Restore the single-string Node-based form:
   `node .devcontainer/ensure-host-config-dirs.cjs`. Node works
   identically in cmd.exe on Windows and bash/zsh on Linux/macOS/WSL,
   and `os.homedir()` respects $HOME on POSIX and %USERPROFILE% on
   Windows. The script is idempotent (mkdirSync recursive is a no-op
   for existing dirs; touch is gated on .gitconfig existence).

   Document Node ≥18 on the host as the only host-side prerequisite
   beyond Docker Desktop and the VS Code Dev Containers extension.
   Anyone running Claude Code on the host already has it.

2. Extend the host-bind mount surface with `~/.ssh` and `~/.config/git`,
   both read-only:

   - `~/.ssh` lets commit signing + push over SSH remotes work inside
     the container without copying private keys. Read-only mount means
     container code can read keys but can't modify or delete them.
     (Threat: a malicious dep can still read private keys from inside
     the container; the read-only mount narrows write-side blast
     radius, not read-side. Documented in the trust-boundary section.)
   - `~/.config/git` covers XDG-style git config (`~/.config/git/config`,
     `~/.config/git/ignore`, `~/.config/git/attributes`) for users who
     keep settings there instead of `~/.gitconfig`. Read-only, same as
     `~/.gitconfig`.

   Update the CLI-state-sharing table and trust-boundary paragraph to
   reflect the expanded surface.

Re-adds .devcontainer/ensure-host-config-dirs.cjs (deleted before the
OS-keyed attempt).
2026-05-28 13:52:38 +01:00

145 lines
6.6 KiB
JSON

// 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. CLI config dirs — bind-mounted from the host so the developer's
// existing plugins, skills, agents, memory, settings, and credentials
// for Claude Code / Codex / Cursor + git identity + gh auth are
// immediately available inside the container, and changes inside the
// container flow back to the host. ~/.gitconfig is read-only so
// container-side `git config --global` doesn't leak to host config.
// For high-trust environments where host and container should NOT
// share credentials, swap these three CLI bind mounts for
// per-devcontainer named volumes (Anthropic's reference pattern).
//
// 2. Per-instance state — `${devcontainerId}` scoped: history and npm
// cache survive container rebuilds but stay isolated between
// sibling devcontainer instances.
//
// 3. 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, e.g., ~/work/GitNexus vs
// ~/projects/GitNexus). Keeps tree-sitter native binaries and
// onnxruntime off the workspace bind mount (the real Win/Mac perf win).
"mounts": [
"source=${localEnv:HOME}/.claude,target=/home/node/.claude,type=bind",
"source=${localEnv:HOME}/.codex,target=/home/node/.codex,type=bind",
"source=${localEnv:HOME}/.cursor,target=/home/node/.cursor,type=bind",
"source=${localEnv:HOME}/.gitconfig,target=/home/node/.gitconfig,type=bind,readonly",
"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=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).
"containerEnv": {
"CLAUDE_CONFIG_DIR": "/home/node/.claude",
"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"
}