mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-30 01:51:20 +00:00
Add dedicated per-workspace named volumes (mount group 6) for the three
AI CLIs' session/resume state so `claude --resume`, `codex resume`, and
`cursor-agent resume` survive a rebuild, a full delete-and-recreate, and
the `docker volume rm <cli>-config-*` re-login fix:
- Claude -> ~/.claude/projects
- Codex -> ~/.codex/sessions
- Cursor -> ~/.cursor/chats + ~/.cursor/projects
The volumes are SEPARATE from the credential/config volumes and keyed
like the node_modules volumes (${localWorkspaceFolderBasename}-...-
${devcontainerId}), so wiping a config volume to force a re-login no
longer destroys session history. Session state already survived a plain
rebuild (it lived in the config volume); this closes the recreation,
volume-rm, and devcontainerId-change gaps.
Kept container-private (not host bind mounts) deliberately: transcripts
can contain pasted secrets, so a host bind would spill them to host
disk, widen the supply-chain write-through surface, and leak
cross-project transcripts. A commented-out opt-in host-bind block is
included for users who accept that trade-off.
post-create.sh: chown each new volume root explicitly (find -xdev stops
at the config-volume filesystem boundary and won't descend into them),
guarded with `[ -d ] || continue` so a missing root can't abort
provisioning under set -e.
README: document the topology, what survives vs not, the one-time
first-rebuild masking of pre-existing config-volume sessions, updated
rebuild/reset commands, and the trust-boundary impact.
399 lines
25 KiB
JSON
399 lines
25 KiB
JSON
// Devcontainer for GitNexus. It pre-installs Claude Code, the OpenAI Codex
|
|
// CLI, and the Cursor CLI, plus the Node.js native build chain. It works on
|
|
// macOS, Linux, Windows via WSL2, and Windows native. Windows native needs a
|
|
// one-time HOME setup. That setup runs automatically via initializeCommand.
|
|
// See .devcontainer/README.md § Windows 11 setup. Open it with the VS Code
|
|
// Dev Containers extension.
|
|
//
|
|
// For first-time setup, auth flows, and troubleshooting, see
|
|
// .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: a pinned version plus one sha256 hash per CPU arch. The
|
|
// Dockerfile checks the tarball against the hash at build time, so it
|
|
// never runs a remote install script. Bump all three values together.
|
|
// Re-hash each arch with:
|
|
// curl -fSL https://downloads.cursor.com/lab/<ver>/linux/<x64|arm64>/agent-cli-package.tar.gz | sha256sum
|
|
"CURSOR_VERSION": "2026.05.28-a70ca7c",
|
|
"CURSOR_SHA256_X64": "7f8b6a09393e0b84b288cc6952b292fc98d15775f644cc01b0b9aa4f04b268df",
|
|
"CURSOR_SHA256_ARM64": "05a0ab361e038729aba25fe7f407531b3e8432912e499d0bffdf1dda0e7833e9",
|
|
"TZ": "${localEnv:TZ:UTC}"
|
|
}
|
|
},
|
|
|
|
// Runs on the HOST, not the container, before the container is created. We
|
|
// write it as a single string on purpose. The spec treats the single-string
|
|
// form as one command that each OS runs its own way. The object form means
|
|
// "named parallel tasks", not per-OS dispatch. We run it with Node so the
|
|
// same command works in cmd.exe on Windows and in bash/zsh on Linux, macOS,
|
|
// and WSL. The script reads `os.homedir()`, which respects $HOME on
|
|
// Linux/macOS and %USERPROFILE% on Windows. It then creates the host-side
|
|
// bind mount source folders, and it is safe to re-run. Host prerequisite:
|
|
// Node on PATH. That is the only host-side tool needed 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 — this has TWO roles. (a) A read-only stage at
|
|
// /host/.<cli>. On container-create, `post-create.sh` COPIES credentials,
|
|
// identity, and single config files out of it. It is read-only so a
|
|
// container can't write the credential snapshot back to the host.
|
|
// (b) Direct read-write bind mounts of the shareable subfolders (Claude
|
|
// plugins/skills/agents/memory/commands; Codex plugins/prompts/memories/
|
|
// skills; Cursor plugins/rules/commands/agents/skills). These overlay the
|
|
// named volume at their sub-paths. These read-write binds go both ways.
|
|
// Install a plugin in the container and it lands on the host; add one on
|
|
// the host and the container sees it. This write-through is a deliberate
|
|
// trade-off. A compromised npm dependency CAN drop files into your host
|
|
// plugin/agent/skill folders, which the next host session then loads. See
|
|
// README § "Trust boundary, concretely". Credentials never live on this
|
|
// surface. They stay in the volume (role 2).
|
|
//
|
|
// 2. AI CLI container config — one named volume per devcontainer. CODEX_HOME
|
|
// points here. CLAUDE_CONFIG_DIR is left unset on purpose, so it resolves
|
|
// to the default ~/.claude, which is this same path. Credentials,
|
|
// identity, and single config files (.credentials.json,
|
|
// ~/.claude/.claude.json, settings.json, config.toml, cli-config.json,
|
|
// mcp.json) live here with correct Linux permissions. They are NOT
|
|
// bind-mounted, because single-file binds break on Docker Desktop Windows
|
|
// (the EXDEV error — see the SINGLE-FILE note below). Container-managed
|
|
// state (sessions, history, caches, IDE locks) stays separate per
|
|
// devcontainer. So two GitNexus checkouts on the same host can't corrupt
|
|
// each other.
|
|
//
|
|
// 3. Other host config — read-only bind mounts for credential and identity
|
|
// folders that lack the permission-flattening and onboarding-state
|
|
// complications Claude Code has (ssh, aws, azure, git config, plus gh and
|
|
// docker). gh and docker are read-only so a compromised dependency can't
|
|
// rewrite the GitHub token or the Docker credHelper. See the inline note
|
|
// at those mounts. `~/.gitconfig` is not mounted here. VS Code auto-copies
|
|
// it separately.
|
|
//
|
|
// 4. Per-instance state — scoped by `${devcontainerId}`: shell history and
|
|
// the npm cache. These survive rebuilds and stay separate between sibling
|
|
// instances.
|
|
//
|
|
// 5. Per-workspace-name AND per-instance state — the workspace `node_modules`
|
|
// volumes use both `${localWorkspaceFolderBasename}` (so you can spot them
|
|
// in `docker volume ls`) and `${devcontainerId}` (so sibling instances of
|
|
// the same repo never collide). This keeps tree-sitter native binaries and
|
|
// onnxruntime off the workspace bind mount, which is faster on Windows and
|
|
// macOS.
|
|
//
|
|
// 6. Per-workspace session state — dedicated named volumes for each CLI's
|
|
// resume/transcript dirs (Claude projects/, Codex sessions/, Cursor chats/
|
|
// + projects/). Same `${localWorkspaceFolderBasename}` + `${devcontainerId}`
|
|
// keying as group 5, but SEPARATE volumes from the group-2 config volumes.
|
|
// That separation is the point: the `docker volume rm <cli>-config-*`
|
|
// re-login / EACCES fix (README § Rebuild/reset) no longer wipes sessions,
|
|
// so `claude --resume`, `codex resume`, and `cursor-agent resume` survive a
|
|
// rebuild, a full delete-and-recreate, AND that wipe. They overlay the
|
|
// config volume at the session sub-paths (Docker precedence: more specific
|
|
// path wins). Container-private by design — transcripts can hold pasted
|
|
// secrets, so unlike the group-1 plugin/agent binds these add NO host
|
|
// write-through surface and leak no other projects' transcripts. They do
|
|
// NOT survive `docker volume prune`, a `${devcontainerId}` change (moving
|
|
// the checkout, WSL vs native), or a new machine — same tier as group 5.
|
|
// To make sessions host-visible/portable instead, see the commented
|
|
// host-bind block below and README § "Session resume across recreation".
|
|
"mounts": [
|
|
// One named volume per container for credentials and identity state. Each
|
|
// CLI's real `~/.<cli>` config folder lives in a volume. That keeps
|
|
// credentials (with correct Linux 600 permissions) and per-container
|
|
// session state separate from the host. Logging in inside the container
|
|
// and logging in on the host are independent. The bind mounts BELOW these
|
|
// volumes override the volume's contents at the paths they cover. Docker
|
|
// mount precedence is: the more specific path wins.
|
|
"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",
|
|
|
|
// Per-workspace SESSION volumes (mount group 6). These OVERLAY the config
|
|
// volumes above at the session sub-paths so "resume my last session"
|
|
// survives container recreation the way the host-bound plugin dirs do.
|
|
// They are SEPARATE volumes from <cli>-config-${devcontainerId}, so the
|
|
// README's `docker volume rm <cli>-config-${devcontainerId}` re-login fix
|
|
// does not touch them. post-create.sh chowns each one explicitly (its
|
|
// `find -xdev` stops at the config-volume filesystem boundary and won't
|
|
// descend into these).
|
|
//
|
|
// KEEP IN SYNC: if you add/rename/remove a session sub-path, update all
|
|
// three places that name it — (1) the mount line here, (2) the DIRS array
|
|
// in post-create.sh (so its chown covers the volume), and (3) the mount
|
|
// table + Session-resume section in README.md.
|
|
//
|
|
// Claude: projects/ holds <encoded-cwd>/<uuid>.jsonl transcripts plus the
|
|
// sessions-index.json that the `/resume` picker reads. The container cwd is
|
|
// always /workspace (encodes to the `-workspace` subdir), so this is the
|
|
// container's own slice only. Pure JSONL/JSON — no SQLite/WAL, so a volume
|
|
// here is clean. `claude --resume` / `--continue` read straight from it.
|
|
"source=${localWorkspaceFolderBasename}-claude-sessions-${devcontainerId},target=/home/node/.claude/projects,type=volume",
|
|
// Codex: sessions/ holds YYYY/MM/DD/rollout-*.jsonl transcripts. The thread
|
|
// index (state_5.sqlite + -wal/-shm) stays at the ~/.codex root on the
|
|
// config volume — it is a single WAL file we must NOT split onto a host
|
|
// bind. On a recreation that drops the config volume, that index is cleanly
|
|
// absent and Codex rebuilds it from these rollout files on the next start
|
|
// (backfill). See README for the one-time-rebuild and corruption caveats.
|
|
"source=${localWorkspaceFolderBasename}-codex-sessions-${devcontainerId},target=/home/node/.codex/sessions,type=volume",
|
|
// Cursor: chats/{hash}/{uuid}/store.db is one SQLite db per session, each in
|
|
// its own leaf dir — a DIRECTORY volume keeps each db beside its -wal/-shm
|
|
// sidecar, so there is no cross-filesystem single-file hazard. projects/
|
|
// (agent-transcripts) is added too. cursor-agent's on-disk layout is
|
|
// community-reverse-engineered (LOW confidence), so this is best-effort;
|
|
// keeping it container-private means a wrong guess can't corrupt host state.
|
|
"source=${localWorkspaceFolderBasename}-cursor-sessions-${devcontainerId},target=/home/node/.cursor/chats,type=volume",
|
|
"source=${localWorkspaceFolderBasename}-cursor-projects-${devcontainerId},target=/home/node/.cursor/projects,type=volume",
|
|
//
|
|
// OPT-IN: host-shared sessions (like the plugin/skill binds). Uncomment to
|
|
// put transcripts on the host — fully visible and portable, but they then
|
|
// land on host disk and become a write-through surface for a compromised
|
|
// in-container dependency, and the whole-dir binds expose OTHER projects'
|
|
// transcripts to the container. Claude is scoped to /workspace's encoded
|
|
// subdir to limit that leak; Codex/Cursor stores are not project-scoped, so
|
|
// they expose every project. If you enable these, also add the matching
|
|
// source dirs to ensure-host-config-dirs.cjs — to its DIRS array (these are
|
|
// directory binds), not FILES (which is only for single-file bind sources
|
|
// like ~/.claude.json) — so Docker can resolve the binds. Read README
|
|
// § "Session resume across recreation" first.
|
|
// "source=${localEnv:HOME}/.claude/projects/-workspace,target=/home/node/.claude/projects/-workspace,type=bind",
|
|
// "source=${localEnv:HOME}/.codex/sessions,target=/home/node/.codex/sessions,type=bind",
|
|
// "source=${localEnv:HOME}/.cursor/chats,target=/home/node/.cursor/chats,type=bind",
|
|
// "source=${localEnv:HOME}/.cursor/projects,target=/home/node/.cursor/projects,type=bind",
|
|
|
|
// Read-only host stage that post-create.sh copies credentials and
|
|
// identity from on container-create. It is read-only so a container
|
|
// process can't write back to host CLI state files. That write-back is the
|
|
// attack vector we are blocking. Only the credential and identity files
|
|
// are READ here. Shareable content is bind-mounted directly read-write
|
|
// below, not staged here.
|
|
"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",
|
|
|
|
// Direct read-write bind mounts for shareable subfolders and files. These
|
|
// OVERLAY the named volume at their target paths, so reads and writes
|
|
// inside the container go straight to the host. `/plugin marketplace add`
|
|
// in the container means it is installed on the host. A new skill on the
|
|
// host shows up in the container on the next read. We accept the trade-off:
|
|
// a compromised npm dependency can write into host plugin, skill, agent,
|
|
// memory, and command folders.
|
|
// Plugins are split in two. The SOURCE (the marketplaces/ git clones and
|
|
// the cache/ of extracted plugin files) does not depend on absolute paths,
|
|
// so it is bind-mounted both ways. The REGISTRY (known_marketplaces.json,
|
|
// installed_plugins.json, plugin-catalog-cache.json) holds absolute,
|
|
// OS-specific paths (`C:\Users\X\.claude\plugins\...` on Windows,
|
|
// `/Users/x/...` on macOS), so it CANNOT be shared as-is. It stays in the
|
|
// named volume. post-create.sh generates it from the host's versions, with
|
|
// the paths rewritten to the container's Linux paths.
|
|
// These are DIRECTORY binds, so they work both ways. Atomic writes inside
|
|
// a directory work fine, because the temp file and the target are on the
|
|
// same filesystem. Writes under these paths in the container reach the host
|
|
// right away, and host changes are visible to the container right away.
|
|
//
|
|
// Claude: the plugin SOURCE folders (the marketplaces/ git clones and the
|
|
// cache/ of extracted files) do not depend on absolute paths. The registry
|
|
// JSON files at the plugins/ root DO carry paths, so they stay in the
|
|
// volume, and post-create.sh rewrites their paths. See the SINGLE-FILE note
|
|
// below.
|
|
"source=${localEnv:HOME}/.claude/plugins/marketplaces,target=/home/node/.claude/plugins/marketplaces,type=bind",
|
|
"source=${localEnv:HOME}/.claude/plugins/cache,target=/home/node/.claude/plugins/cache,type=bind",
|
|
"source=${localEnv:HOME}/.claude/skills,target=/home/node/.claude/skills,type=bind",
|
|
"source=${localEnv:HOME}/.claude/agents,target=/home/node/.claude/agents,type=bind",
|
|
"source=${localEnv:HOME}/.claude/memory,target=/home/node/.claude/memory,type=bind",
|
|
"source=${localEnv:HOME}/.claude/commands,target=/home/node/.claude/commands,type=bind",
|
|
//
|
|
// Codex: bind the WHOLE plugins/ folder (the parent of cache/). `codex
|
|
// plugin add` stages installs INSIDE plugins/cache/<marketplace>/ and
|
|
// renames within that folder (confirmed with strace). So under a single
|
|
// bind of plugins/, the rename stays on the same filesystem and never hits
|
|
// the EXDEV cross-filesystem error. Unlike Claude, Codex has NO path-
|
|
// bearing registry file under plugins/. Enablement lives in config.toml at
|
|
// the .codex root, as git URLs, not filesystem paths. So nothing needs
|
|
// rewriting and the whole folder can be bound. `.tmp/` is the ext4 staging
|
|
// area for marketplace clones, and it is deliberately NOT bound. It must
|
|
// stay on the volume as the source side of the cross-filesystem copy.
|
|
"source=${localEnv:HOME}/.codex/plugins,target=/home/node/.codex/plugins,type=bind",
|
|
"source=${localEnv:HOME}/.codex/prompts,target=/home/node/.codex/prompts,type=bind",
|
|
"source=${localEnv:HOME}/.codex/memories,target=/home/node/.codex/memories,type=bind",
|
|
"source=${localEnv:HOME}/.codex/skills,target=/home/node/.codex/skills,type=bind",
|
|
//
|
|
// Cursor: cursor-agent (the CLI, not just the IDE) shares the Cursor 2.5
|
|
// plugin/rules/commands/agents/skills files on disk. Bind the SOURCE
|
|
// folders, which don't depend on absolute paths.
|
|
// plugins/installed_plugins.json carries absolute Windows paths, like
|
|
// Claude's registry, so it stays in the volume and post-create.sh rewrites
|
|
// it. That is why the plugins/ sub-folders are bound one by one instead of
|
|
// binding the whole plugins/ folder. mcp.json and hooks.json are single
|
|
// files, which are unsafe to bind (the EXDEV error), so they are copied on
|
|
// create instead. hooks.json is also NOT synced at all. Cursor hooks run
|
|
// shell commands on a timer or event, with no user action, so a poisoned
|
|
// host hooks.json would run by itself in the container. NOTE this is a
|
|
// partial fix, not a clean boundary. The read-write bound
|
|
// commands/agents/skills/rules folders (here and for Claude) are also
|
|
// surfaces that hold instructions or run shell commands, and a compromised
|
|
// dependency can write through them to the host. Skipping hooks.json just
|
|
// removes the one surface that fires WITHOUT an agent choosing to run it.
|
|
// The wider write-through trade-off is accepted and documented in README
|
|
// § Trust boundary. To fully close it, switch these folders to copy-on-
|
|
// create too.
|
|
"source=${localEnv:HOME}/.cursor/plugins/marketplaces,target=/home/node/.cursor/plugins/marketplaces,type=bind",
|
|
"source=${localEnv:HOME}/.cursor/plugins/local,target=/home/node/.cursor/plugins/local,type=bind",
|
|
"source=${localEnv:HOME}/.cursor/rules,target=/home/node/.cursor/rules,type=bind",
|
|
"source=${localEnv:HOME}/.cursor/commands,target=/home/node/.cursor/commands,type=bind",
|
|
"source=${localEnv:HOME}/.cursor/agents,target=/home/node/.cursor/agents,type=bind",
|
|
"source=${localEnv:HOME}/.cursor/skills,target=/home/node/.cursor/skills,type=bind",
|
|
//
|
|
// SINGLE-FILE binds for settings.json, .claude.json, and config.toml are
|
|
// deliberately ABSENT. On Docker Desktop Windows the named volume sits on
|
|
// one filesystem (ext4, /dev/sdd) and a single-file bind from the host sits
|
|
// on another (the 9p drvfs share). Apps save a config by writing `foo.tmp`
|
|
// and renaming it over `foo`. That rename can't cross filesystems: it hits
|
|
// the EXDEV error and fails with `Device or resource busy` or `inter-device
|
|
// move failed`. Codex's TUI shows this as "config/batchWrite failed in
|
|
// TUI"; Claude just silently loses the write the same way. Instead, we use
|
|
// a read-only host stage at /host/.claude, and post-create.sh copies these
|
|
// files into the named volume on every container-create. Host changes show
|
|
// up on the next rebuild. Container changes stay inside the container until
|
|
// a rebuild.
|
|
"source=${localEnv:HOME}/.claude.json,target=/host/.claude.json,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",
|
|
// gh and docker are READ-ONLY. The container reads your EXISTING host login,
|
|
// which is the common case. But a compromised in-container dependency can't
|
|
// rewrite ~/.config/gh/hosts.yml (your GitHub token) or
|
|
// ~/.docker/config.json (the registry credHelper, which points at a
|
|
// binary). The trade-off: `gh auth login` or `docker login` run INSIDE the
|
|
// container won't persist back to the host. Re-run them on the host, or
|
|
// remove `,readonly` from the next two lines if you want in-container logins
|
|
// to stick. See README § Trust boundary.
|
|
"source=${localEnv:HOME}/.config/gh,target=/home/node/.config/gh,type=bind,readonly",
|
|
"source=${localEnv:HOME}/.docker,target=/home/node/.docker,type=bind,readonly",
|
|
"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 way to authenticate for all three CLIs.
|
|
// Credentials live in the per-container named volumes (claude-config,
|
|
// codex-config, cursor-config), NOT in the host bind mounts. They are copied
|
|
// from the read-only /host/.<cli> stage into the volume on container-create.
|
|
// Single-file binds would break on Docker Desktop Windows (the EXDEV error).
|
|
// Only shareable content (plugins, skills, agents, memory, commands) is
|
|
// bound read-write to the host ~/.claude, ~/.codex, and ~/.cursor folders
|
|
// declared in the mounts block above.
|
|
// API keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, CURSOR_API_KEY) are NOT
|
|
// injected via containerEnv. `${localEnv:VAR}` turns an unset host var into
|
|
// an empty string. Cursor in particular treats `CURSOR_API_KEY=""` as "use
|
|
// this empty key" instead of "fall back to the stored login", which would
|
|
// silently break `cursor-agent login`. If you need API-key auth, `export`
|
|
// the var in your container shell, or carry it in your VS Code dotfiles repo
|
|
// (see .devcontainer/README.md).
|
|
// CLAUDE_CONFIG_DIR is left unset on purpose. The Claude default is
|
|
// `$HOME/.claude` (= `/home/node/.claude`), which is exactly where the
|
|
// claude-config named volume mounts. Setting the env var would change 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 holds `hasCompletedOnboarding`, the user-scope MCP config, and
|
|
// per-project trust. Leaving the var unset matches host behavior. It also
|
|
// lets post-create.sh's sync of `$HOME/.claude.json` skip the setup 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, this env var makes the
|
|
// dependency explicit instead of silently following the default.
|
|
"containerEnv": {
|
|
"CODEX_HOME": "/home/node/.codex",
|
|
"DISABLE_AUTOUPDATER": "1",
|
|
// post-create.sh removes `installMethod` from the seeded ~/.claude.json so
|
|
// the npm-global binary detects its own install method. This is a backup
|
|
// safeguard for Claude Code issue #17289. The install-checks routine probes
|
|
// ~/.local/bin/claude just because that directory EXISTS. It does exist
|
|
// here, because Cursor drops agent and cursor-agent symlinks there. So even
|
|
// when installMethod is non-native, the routine reports a false "claude
|
|
// command not found at ~/.local/bin/claude". DISABLE_AUTOUPDATER does NOT
|
|
// turn that routine off. DISABLE_INSTALLATION_CHECKS is its dedicated kill
|
|
// switch.
|
|
"DISABLE_INSTALLATION_CHECKS": "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" }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// Do not remap port 4747 (gitnexus serve). gitnexus-web hardcodes
|
|
// http://localhost:4747 as its 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"
|
|
}
|
|
},
|
|
|
|
// Lifecycle split (from the Dev Container spec):
|
|
// - `updateContentCommand` runs on container-create AND whenever the
|
|
// workspace content changes, such as a lockfile update. It owns installing
|
|
// the workspace dependencies. Re-installing on every container-create
|
|
// wastes time when nothing changed, but it must re-run when deps change.
|
|
// - `postCreateCommand` runs once on container-create. It owns syncing the
|
|
// AI CLI credentials and identity from the host. That work should happen
|
|
// exactly once per container instance, not on every content update.
|
|
// Run both with an explicit `bash` so they don't depend on the script's
|
|
// executable bit surviving the workspace bind mount.
|
|
"updateContentCommand": "bash .devcontainer/install-deps.sh",
|
|
"postCreateCommand": "bash .devcontainer/post-create.sh"
|
|
}
|