mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-29 01:41:42 +00:00
feat(devcontainer): bind-mount host CLI config dirs for plugin/skill/memory sync
Switch the credential/config mounts from per-devcontainer named volumes
to bind mounts of `${localEnv:HOME}/.claude`, `~/.codex`, and
`~/.cursor`. Effect inside the container:
- Authentication is shared with the host. If you've already run
`claude login` / `codex login --device-auth` / `cursor-agent login`
on the host, you're already authenticated in the container.
- Plugins, skills, agents, memory, and settings sync both ways. Install
a plugin in the container, it shows up on the host; add a custom
agent on the host, the container sees it immediately.
- All devcontainers on the host share the same CLI state, mirroring
how host shells already share it. (Per-workspace isolation of plugins
was never a stated requirement; the previous per-devcontainer named
volumes leaked nothing useful.)
Add `.devcontainer/ensure-host-config-dirs.cjs` and wire it as
`initializeCommand`. It runs on the host before container create and
guarantees `~/.claude`, `~/.codex`, `~/.cursor` exist, so Docker doesn't
reject the bind mount when a CLI has never been used on this host.
Cross-platform via Node `os.homedir()` + `fs.mkdirSync({recursive: true})`;
idempotent; no third-party deps.
Update `.devcontainer/README.md`:
- New "How CLI state is shared with your host" section explaining the
bind-mount model up front so users know their host plugins/skills/
memory carry into the container.
- Mark first-time-login section as skippable when the user is already
authenticated on the host.
- Note the high-trust escape hatch: replace the three bind mounts with
`type=volume` named volumes if the host/container trust boundary
needs to be separated (Anthropic's reference pattern for enterprise).
- Replace the obsolete "rm named volume" troubleshooting row with one
that covers EACCES/EPERM on the host-bind-mount path.
This commit is contained in:
parent
a75fc2e060
commit
35e61a9e66
3 changed files with 68 additions and 12 deletions
|
|
@ -43,9 +43,23 @@ Open the repo folder in VS Code → **Reopen in Container**. The image is multi-
|
|||
|
||||
Same as macOS — open in VS Code and reopen in container. `updateRemoteUserUID: true` (default) shifts the container's `node` user UID/GID to match your host user, so bind-mounted files stay writable without extra setup.
|
||||
|
||||
## How CLI state is shared with your host
|
||||
|
||||
`~/.claude`, `~/.codex`, and `~/.cursor` inside the container are **bind-mounted directly from your host's `$HOME`**. That means:
|
||||
|
||||
- **Authentication is shared.** If you're already logged in on the host (`claude login`, `codex login`, `cursor-agent login`), you're already logged in inside the container. No second login step.
|
||||
- **Plugins, skills, agents, memory, and settings sync both ways.** Install a plugin from inside the container and it shows up on the host; add a custom agent on the host and the container sees it immediately. The auto-memory store at `~/.claude/projects/<workspace>/memory/` is the same file tree from both sides.
|
||||
- **No per-workspace duplication.** All your devcontainers across all your projects see the same `.claude`/`.codex`/`.cursor` content, just like all your host shells do.
|
||||
|
||||
The bind mount source paths are guaranteed to exist by `.devcontainer/ensure-host-config-dirs.cjs`, which `initializeCommand` runs on the host before the container is created.
|
||||
|
||||
For a high-trust enterprise environment where you don't want container code to be able to touch host credentials, replace the three `type=bind` entries for `.claude`/`.codex`/`.cursor` in `.devcontainer/devcontainer.json` with `type=volume` named volumes (Anthropic's reference pattern). Most personal-dev setups don't need that isolation — host and container share the same trust boundary.
|
||||
|
||||
## First-time CLI authentication
|
||||
|
||||
**Interactive login is the default for all three CLIs.** Credentials persist in per-workspace named volumes scoped by `${devcontainerId}`, so you authenticate once per project — subsequent rebuilds reuse the stored credentials.
|
||||
If you already use these CLIs on the host, **skip this section** — your existing logins are already in scope inside the container.
|
||||
|
||||
If a CLI is brand-new on this host, log in from inside _or_ outside the container; either populates the shared `~/.<cli>` directory.
|
||||
|
||||
### Claude Code
|
||||
|
||||
|
|
@ -53,7 +67,7 @@ Same as macOS — open in VS Code and reopen in container. `updateRemoteUserUID:
|
|||
claude login
|
||||
```
|
||||
|
||||
Opens a browser auth flow. VS Code's port forwarding handles the OAuth callback automatically. After auth, `~/.claude/` is populated in the named volume and persists across rebuilds. The `DISABLE_AUTOUPDATER=1` env var prevents the running CLI from updating itself — rebuild the container to pick up a newer Claude Code.
|
||||
Opens a browser auth flow. VS Code's port forwarding handles the OAuth callback automatically. After auth, `~/.claude/` is populated and visible from both host and container. The `DISABLE_AUTOUPDATER=1` env var prevents the in-container CLI from auto-updating — rebuild the container to pick up a newer Claude Code.
|
||||
|
||||
### OpenAI Codex CLI
|
||||
|
||||
|
|
@ -61,7 +75,7 @@ Opens a browser auth flow. VS Code's port forwarding handles the OAuth callback
|
|||
codex login --device-auth
|
||||
```
|
||||
|
||||
The device-code flow prints a URL and a one-time code. Visit the URL on your host browser, paste the code, and the CLI authenticates without needing a callback listener — this is the most reliable path inside containers. Credentials persist in `~/.codex/auth.json` inside the named volume.
|
||||
The device-code flow prints a URL and a one-time code. Visit the URL on your host browser, paste the code, and the CLI authenticates without needing a callback listener — this is the most reliable path inside containers. Credentials land in `~/.codex/auth.json` (shared with host).
|
||||
|
||||
`codex login` (browser-callback variant) also works but can be flaky in some headless contexts; prefer `--device-auth`.
|
||||
|
||||
|
|
@ -71,7 +85,7 @@ The device-code flow prints a URL and a one-time code. Visit the URL on your hos
|
|||
cursor-agent login
|
||||
```
|
||||
|
||||
Opens a browser auth flow; VS Code's port forwarding handles the callback. After auth, credentials persist in `~/.cursor/cli-config.json` inside the named volume.
|
||||
Opens a browser auth flow; VS Code's port forwarding handles the callback. Credentials persist in `~/.cursor/cli-config.json` (shared with host).
|
||||
|
||||
Verify any time with `cursor-agent status`.
|
||||
|
||||
|
|
@ -146,7 +160,7 @@ Three build args control pinned versions:
|
|||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|--------------|-----|
|
||||
| `EACCES` on first `claude login` / `codex login` / `cursor-agent login` | Named volume mount got a stale state | `docker volume rm` the relevant `*-config-<devcontainerId>` volume and rebuild |
|
||||
| `EACCES` / `EPERM` writing into `~/.claude`, `~/.codex`, or `~/.cursor` inside the container | Windows-side bind-mount permission translation got out of sync after a UID change between rebuilds | On the host, ensure your user owns the directory tree; if it's truly stuck, move the affected dir aside and let the CLI rebuild it (`mv ~/.claude ~/.claude.bak` and log in again). Long-term: clone in WSL2 — bind-mount permission classes don't apply to WSL-side filesystems |
|
||||
| `EPERM: operation not permitted, copyfile ... '.husky/_/h'` in `postCreateCommand` | Leftover `.husky/_/` from a previous container run; Docker Desktop's Windows bind mount won't let the new container's `node` user overwrite it. `postCreateCommand` already runs `rm -rf .husky/_` defensively, but if you hit it on an older config, delete `.husky/_/` on the host (`rm -rf .husky/_`) and rebuild | Long-term: clone the repo inside WSL2 (see [Windows 11 WSL2 setup](#windows-11-primary-host--wsl2-setup)) — WSL-side filesystems don't have this bind-mount class of issue |
|
||||
| Vite never hot-reloads on Windows | Repo cloned on Windows side, not WSL2 | Re-clone inside WSL2 (see [WSL2 setup](#windows-11-primary-host--wsl2-setup)) |
|
||||
| `gitnexus-web` can't reach the backend | `4747` was remapped or backend isn't running | Verify the Ports panel shows `4747` forwarded with no remap; start the backend with `cd gitnexus && npx gitnexus serve` |
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@
|
|||
}
|
||||
},
|
||||
|
||||
// Runs on the HOST before the container is created. Ensures the bind
|
||||
// mount sources (~/.claude, ~/.codex, ~/.cursor) exist so Docker doesn't
|
||||
// reject the mount when a CLI has never been used on this host. Safe
|
||||
// re-run; idempotent. See ensure-host-config-dirs.cjs.
|
||||
"initializeCommand": "node .devcontainer/ensure-host-config-dirs.cjs",
|
||||
|
||||
// Anthropic's official Claude Code Feature pulls the latest stable at
|
||||
// build time; DISABLE_AUTOUPDATER below locks it inside the running
|
||||
// container so rebuild is the only way the version changes.
|
||||
|
|
@ -32,14 +38,25 @@
|
|||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
|
||||
"workspaceFolder": "/workspace",
|
||||
|
||||
// Named volumes scoped per-devcontainer keep auth tokens, history, and
|
||||
// node_modules persistent across rebuilds without leaking between
|
||||
// workspaces. Sub-workspace node_modules volumes keep tree-sitter native
|
||||
// binaries and onnxruntime off the bind mount (the real Win/Mac perf win).
|
||||
// CLI config dirs are bind-mounted from the host so the developer's
|
||||
// existing plugins, skills, agents, memory, settings, and credentials
|
||||
// for Claude Code / Codex / Cursor are immediately available inside the
|
||||
// container, and changes inside the container flow back to the host.
|
||||
// The `initializeCommand` above guarantees these source paths exist on
|
||||
// first up so Docker never errors out on a missing bind source. Anthropic
|
||||
// recommends per-devcontainer named volumes instead in enterprise /
|
||||
// high-trust environments; for personal dev where the host + container
|
||||
// share the same trust boundary, bind mounts give the better daily-driver
|
||||
// experience.
|
||||
//
|
||||
// Shell history, npm cache, and node_modules stay in named volumes
|
||||
// scoped per-workspace — history doesn't need to escape the workspace,
|
||||
// node_modules belong off the bind mount for Win/Mac perf, and the npm
|
||||
// cache wants the container-native FS.
|
||||
"mounts": [
|
||||
"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}/.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=commandhistory-${devcontainerId},target=/commandhistory,type=volume",
|
||||
"source=npm-cache-${devcontainerId},target=/home/node/.npm,type=volume",
|
||||
"source=${localWorkspaceFolderBasename}-root-node-modules,target=/workspace/node_modules,type=volume",
|
||||
|
|
|
|||
25
.devcontainer/ensure-host-config-dirs.cjs
Normal file
25
.devcontainer/ensure-host-config-dirs.cjs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Runs on the HOST (not inside the container) before the dev container is
|
||||
// created, as the devcontainer.json `initializeCommand`. Ensures the host
|
||||
// has empty config directories for Claude Code, Codex CLI, and Cursor CLI
|
||||
// at the user's home directory so the bind mounts in devcontainer.json
|
||||
// always have a real source path (Docker fails the bind mount if the
|
||||
// source doesn't exist).
|
||||
//
|
||||
// Cross-platform via Node's `os.homedir()` and `fs.mkdirSync({recursive:
|
||||
// true})`. Node is already required on the host because the project's
|
||||
// Claude Code, the @devcontainers/cli reentry, and most repo scripts
|
||||
// depend on it.
|
||||
//
|
||||
// Safe to re-run: `recursive: true` is a no-op when the directory exists.
|
||||
//
|
||||
// No third-party dependencies; CommonJS so it runs on any Node ≥ 12
|
||||
// without ESM gymnastics.
|
||||
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
for (const dir of [".claude", ".codex", ".cursor"]) {
|
||||
const target = path.join(os.homedir(), dir);
|
||||
fs.mkdirSync(target, { recursive: true });
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue