mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-04 02:31:36 +00:00
refactor(devcontainer): hybrid RW bind + per-container creds — fixes EROFS on in-container plugin install
The previous Option B topology (RO host stage + named volume + symlinks
into the volume) made `/plugin marketplace add` inside the container fail
with EROFS — the symlinks pointed at a read-only mount, so Claude
couldn't create new marketplace dirs. Switch to a hybrid: shareable
content (plugins/skills/agents/memory/commands/settings.json/$HOME/.claude.json
for Claude; config.toml/memories/skills for Codex) gets a direct RW bind
from host so reads and writes go bidirectionally; credentials + the
small identity file stay in per-container named volumes so logout in
container doesn't log out host.
Mount precedence does the heavy lifting: the named volume mounts at
/home/node/.<cli> first, then sub-path bind mounts overlay specific
sub-paths. Container's view at /home/node/.claude/plugins/ is the host
dir; container's view at /home/node/.claude/.credentials.json is the
named volume's file.
What this gives you:
- /plugin marketplace add in container = installed on host
- New skill on host = visible in container immediately (no rebuild)
- claude logout in container = host stays logged in
- compound-engineering plugin enabled on host = enabled in container
- Theme picker fires once (or never if host has theme set)
What it costs:
- Write-through: a compromised npm dep in workspace deps can write to
host ~/.claude/{plugins,skills,agents,memory,commands}/. Documented
trade-off; for personal dev, accepted. Credentials still per-container.
post-create.sh becomes much simpler — only syncs the four credential
files from host into the named volumes. No more symlink dance, no more
state-file merging.
ensure-host-config-dirs.cjs gains the new bind sources: the shareable
subdirs and settings.json/config.toml files get mkdir/touched on host
so Docker doesn't reject the mount when a CLI has never been used.
This commit is contained in:
parent
26470d5580
commit
b43fc21c98
4 changed files with 122 additions and 138 deletions
|
|
@ -76,44 +76,46 @@ Same as macOS — open in VS Code and reopen in container. `updateRemoteUserUID:
|
|||
|
||||
## How CLI state is shared with your host
|
||||
|
||||
### AI CLIs (Claude Code, Codex, Cursor): read-only host share + per-container credentials
|
||||
### AI CLIs (Claude Code, Codex, Cursor): direct RW bind for shareable content + per-container credentials
|
||||
|
||||
The three AI CLIs use a **hybrid topology** so you get host plugins/skills/memory inside the container without re-installing anything, but each container manages its own credentials with proper Linux permissions:
|
||||
The three AI CLIs use a **hybrid mount topology**: shareable subdirs/files (plugins, skills, agents, memory, commands, settings) are RW bind-mounted from host so reads AND writes go bidirectionally; credentials + identity stay in per-container named volumes so logging in/out in the container doesn't affect the host. Bind mounts at sub-paths override the named volume's content at those paths (Docker mount precedence — more specific path wins).
|
||||
|
||||
| Mount | Source | Target | Mode |
|
||||
|---|---|---|---|
|
||||
| Host Claude state, read-only stage | `$HOME/.claude` | `/host/.claude` | **read-only** bind |
|
||||
| Host Codex state, read-only stage | `$HOME/.codex` | `/host/.codex` | **read-only** bind |
|
||||
| Host Cursor state, read-only stage | `$HOME/.cursor` | `/host/.cursor` | **read-only** bind |
|
||||
| Host onboarding state | `$HOME/.claude.json` | `/host/.claude.json` | **read-only** bind |
|
||||
| Container Claude config dir | _named volume_ `claude-config-${devcontainerId}` | `/home/node/.claude` (`CLAUDE_CONFIG_DIR`) | read-write |
|
||||
| Container Codex config dir | _named volume_ `codex-config-${devcontainerId}` | `/home/node/.codex` (`CODEX_HOME`) | read-write |
|
||||
| Container Cursor config dir | _named volume_ `cursor-config-${devcontainerId}` | `/home/node/.cursor` | read-write |
|
||||
| Mount | Source | Target | Mode | Purpose |
|
||||
|---|---|---|---|---|
|
||||
| Container Claude config dir | _named volume_ `claude-config-${devcontainerId}` | `/home/node/.claude` | rw | Per-container credentials + identity |
|
||||
| Container Codex config dir | _named volume_ `codex-config-${devcontainerId}` | `/home/node/.codex` | rw | Per-container credentials |
|
||||
| Container Cursor config dir | _named volume_ `cursor-config-${devcontainerId}` | `/home/node/.cursor` | rw | Per-container credentials |
|
||||
| Host Claude state, read-only stage | `$HOME/.claude` | `/host/.claude` | **read-only** | `post-create.sh` reads credentials + identity from here on container-create |
|
||||
| Host Codex state, read-only stage | `$HOME/.codex` | `/host/.codex` | **read-only** | Same purpose for Codex |
|
||||
| Host Cursor state, read-only stage | `$HOME/.cursor` | `/host/.cursor` | **read-only** | Same purpose for Cursor |
|
||||
| **Claude shareable subdirs** (overlay on the volume) | `$HOME/.claude/{plugins,skills,agents,memory,commands}` | `/home/node/.claude/{plugins,skills,agents,memory,commands}` | rw | **Bidirectional** — install plugin on host or in container, both sides see it |
|
||||
| **Claude shareable files** (overlay on the volume) | `$HOME/.claude/settings.json`, `$HOME/.claude.json` | `/home/node/.claude/settings.json`, `/home/node/.claude.json` | rw | Theme, enabled plugins, MCP user-scope, `hasCompletedOnboarding`, project trust — all shared |
|
||||
| **Codex shareable** | `$HOME/.codex/{config.toml,memories,skills}` | `/home/node/.codex/{config.toml,memories,skills}` | rw | Symmetric with Claude's shareable surface |
|
||||
|
||||
`post-create.sh` populates the named volumes on **every container-create** (rebuild — not container start):
|
||||
**What gets shared bidirectionally (RW bind from host):**
|
||||
|
||||
- **Symlinks shared subdirs from the read-only host stage** into the container's config volume, so installing a plugin on the host shows up in the container after a rebuild. The shared list:
|
||||
- **Claude**: `plugins/`, `skills/`, `agents/`, `memory/`, `commands/` — your user-installed surface
|
||||
- **Codex**: `config.toml`, `memories/`, `skills/` — your prefs + user-installed surface (symmetric with Claude)
|
||||
- **Cursor**: nothing shared via symlink (Cursor's `cli-config.json` conflates auth + settings; no separate plugin surface)
|
||||
- **Syncs these from host into the container's config volume** (not symlinks — container can refresh/rewrite freely, host stays untouched). Sync is "always overwrite if host has the file, otherwise leave container alone", so logging in on host populates the container on next rebuild, and logging in only inside the container keeps that login (host has no source to overwrite from):
|
||||
- `.credentials.json` (Claude), `auth.json` (Codex), `cli-config.json` (Cursor) — credentials
|
||||
- **Two Claude state files**: `$HOME/.claude.json` (carries `hasCompletedOnboarding`, MCP user-scope config, project trust, `tipsHistory`) **and** `~/.claude/.claude.json` (carries `userID`, `oauthAccount`, migration tracking). Both files get synced. We deliberately leave `CLAUDE_CONFIG_DIR` unset (Claude's default `~/.claude` matches the named-volume mount target) so Claude reads onboarding state from `$HOME/.claude.json` — which is where `hasCompletedOnboarding` lives. With `CLAUDE_CONFIG_DIR` set, Claude would instead read the small identity-only file and re-onboard every container. Stub fallback `{"hasCompletedOnboarding":true,"installMethod":"global"}` written to `$HOME/.claude.json` only if the host had neither file.
|
||||
- **`settings.json`** (Claude) — theme, `enabledPlugins`, and `extraKnownMarketplaces`. Without this synced, theme picker fires on every fresh volume and host-installed plugins stay disabled even though their files are symlinked in. We pin Claude Code via `CLAUDE_CODE_VERSION`, so version drift between host (floating) and container (pinned) is bounded — Claude tolerates unknown keys, and we re-sync on every container-create anyway.
|
||||
- **Claude**: `plugins/`, `skills/`, `agents/`, `memory/`, `commands/` (the user-installed surface), `settings.json` (theme + `enabledPlugins` + `extraKnownMarketplaces`), `$HOME/.claude.json` (`hasCompletedOnboarding` + MCP user-scope + per-project trust + activity counters)
|
||||
- **Codex**: `config.toml` (prefs), `memories/` + `skills/` (user-installed surface)
|
||||
- **Cursor**: nothing structural (Cursor doesn't expose plugin/skill/agent dirs — `cli-config.json` conflates auth+settings and stays per-container)
|
||||
|
||||
**Why read-only stage + named volume instead of a single host bind mount:**
|
||||
Install a plugin on the host or inside the container — both sides see it immediately. Run `/plugin marketplace add` from inside the container and it lands in your host `~/.claude/plugins/marketplaces/`. Save a memory via the `/remember` skill from either side and it persists to the same host file.
|
||||
|
||||
- **No host filesystem write-through.** A compromised npm package inside the container can't drop `plugins/evil/` or `agents/evil.md` into your host config — the read-only mount blocks the write. Without this, the container is a code-execution escape vector that persists after teardown (next host Claude session would auto-load the malicious agent).
|
||||
- **Proper credential perms.** Docker Desktop's Windows bind mount surfaces every host file as `root:root` mode `777`. Named-volume files inside the container come with proper Linux ownership and `chmod 600` for credentials — what Claude Code, Codex, and Cursor expect.
|
||||
- **Skips host/container lock-file and ghost-project collisions.** We deliberately do NOT symlink `~/.claude/ide/` (per-process IDE lock files would collide between host and container Claude Code instances), `~/.claude/projects/` (host encodes workspace as `D--development-coding-GitNexus`, container as `-workspace` — symlinking creates two ghost project trees with split memory), or `~/.claude/settings.json` (container is pinned, host floats — bidirectional writes cause silent schema drift).
|
||||
**What stays per-container (in the named volume) and is synced from host on container-create:**
|
||||
|
||||
**What this means for your workflow:**
|
||||
- `.credentials.json` (Claude OAuth tokens), `auth.json` (Codex), `cli-config.json` (Cursor) — credentials
|
||||
- `~/.claude/.claude.json` (Claude's identity-only file: `userID`, `oauthAccount`, migration tracking) — kept per-container so logging in via container doesn't overwrite host's stored identity
|
||||
|
||||
- Install a plugin on the **host** → rebuild container → it's inside the container.
|
||||
- Install a plugin **inside the container** → it lives only in that container's named volume; the host is unaffected. Re-install on host if you want it there too.
|
||||
- **Log in on host OR inside the container — both work.** Logging in on host populates the matching file (`.credentials.json` / `auth.json` / `cli-config.json`) under your `$HOME/.<cli>/`, which the next container-create syncs in. Logging in only inside the container writes to the named volume, which persists across rebuilds (the host has nothing to sync over the top of). The named volume is keyed by `${devcontainerId}` — stable for a given workspace folder path, so the in-container login survives ordinary rebuilds.
|
||||
- `claude logout` inside the container clears the named volume's credentials; the host's `.credentials.json` is untouched. Next container-create re-syncs from host if host is logged in.
|
||||
- **Refresh-token divergence between rebuilds.** Container's credentials match host's at container-create time; after that, container manages its own refresh until the next rebuild. Anthropic rotates refresh tokens on every use, so an unattended container that hasn't talked to the API in weeks can hit a silent 401 if the host has refreshed since. Re-run `claude login` inside the container, or rebuild, to recover.
|
||||
`post-create.sh` runs on every container-create, copies host's credentials into the volume if present, then container manages refresh from there. Sync is "always overwrite if host has the file, otherwise leave container alone". So:
|
||||
|
||||
- Host has credentials → container starts logged in.
|
||||
- Host has no credentials → `claude login` / `codex login --device-auth` / `cursor-agent login` inside container; credentials stay in the named volume across rebuilds (volume is keyed by `${devcontainerId}`, stable for the workspace path).
|
||||
- `claude logout` inside container clears volume credentials only; host is untouched.
|
||||
|
||||
**Why CLAUDE_CONFIG_DIR is intentionally NOT set:** Claude's default `~/.claude` matches the named-volume mount target, so the env var added no behavior — but setting it changed which file Claude reads `hasCompletedOnboarding` from. With it set, Claude reads `$CLAUDE_CONFIG_DIR/.claude.json` (the small identity-only file) and re-onboards every container; without it, Claude reads `$HOME/.claude.json` (now bind-mounted from host, with `hasCompletedOnboarding: true`).
|
||||
|
||||
**Trade-off accepted: write-through to host CLI config.** A compromised npm package in the workspace dep tree, running inside the container, can write to `~/.claude/plugins/`, `~/.claude/agents/`, `~/.claude/memory/`, etc. on host. The next host Claude session would auto-load whatever it dropped. This is the explicit cost of the bidirectional RW bind. The alternative (read-only stage + symlinks) made `/plugin marketplace add` inside the container fail with EROFS — unacceptable. Credentials stay in the per-container named volume, so an attacker has to compromise the OAuth-bearing file specifically in container to get them; the volume isn't shared back to host.
|
||||
|
||||
**Refresh-token divergence between rebuilds.** Container's credentials match host's at container-create time; after that, container manages its own refresh until the next rebuild. Anthropic rotates refresh tokens on every use, so an unattended container that hasn't talked to the API in weeks can hit a silent 401 if the host has refreshed since. Re-run `claude login` inside the container, or rebuild, to recover.
|
||||
|
||||
### Other host bind mounts
|
||||
|
||||
|
|
|
|||
|
|
@ -74,13 +74,42 @@
|
|||
// 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",
|
||||
// Per-container named volumes for credentials + identity state. Each
|
||||
// CLI's actual `~/.<cli>` config dir lives in a volume so credentials
|
||||
// (with proper Linux 600 perms) and per-container session state stay
|
||||
// isolated from the host. Login in container vs login on host =
|
||||
// independent. Bind mounts BELOW these volumes override the volume's
|
||||
// contents at the bound sub-paths — Docker mount precedence: 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",
|
||||
|
||||
// Read-only host stage for post-create.sh to copy credentials +
|
||||
// identity from on container-create. Kept read-only so a container
|
||||
// process can't write back to host CLI state files (the write-through
|
||||
// attack vector). Only the credential/identity files are READ here;
|
||||
// shareable content is bind-mounted directly RW below, not staged.
|
||||
"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 RW bind mounts for shareable subdirs + files. These OVERLAY
|
||||
// the named volume at their target paths, so reads/writes from inside
|
||||
// the container go straight to host. `/plugin marketplace add` in
|
||||
// container = installed on host. New skill on host = visible in
|
||||
// container next read. Trade-off accepted: a compromised npm dep can
|
||||
// write into host plugin/skill/agent/memory/command dirs.
|
||||
"source=${localEnv:HOME}/.claude/plugins,target=/home/node/.claude/plugins,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",
|
||||
"source=${localEnv:HOME}/.claude/settings.json,target=/home/node/.claude/settings.json,type=bind",
|
||||
"source=${localEnv:HOME}/.claude.json,target=/home/node/.claude.json,type=bind",
|
||||
"source=${localEnv:HOME}/.codex/config.toml,target=/home/node/.codex/config.toml,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",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -82,9 +82,23 @@ if (process.platform === "win32" && !process.env.HOME) {
|
|||
|
||||
const home = os.homedir();
|
||||
|
||||
for (const dir of [
|
||||
// Directory bind-mount sources. devcontainer.json declares RW binds for
|
||||
// shareable subdirs (plugins/skills/agents/memory/commands for Claude;
|
||||
// memories/skills for Codex) so reads/writes go directly host<->container.
|
||||
// Docker rejects bind mounts whose source doesn't exist — mkdir -p each
|
||||
// one. Per-CLI directories themselves (~/.claude, ~/.codex, ~/.cursor)
|
||||
// are also created for the /host/.<cli> read-only stage mounts that
|
||||
// post-create.sh reads credentials from.
|
||||
const dirs = [
|
||||
".claude",
|
||||
path.join(".claude", "plugins"),
|
||||
path.join(".claude", "skills"),
|
||||
path.join(".claude", "agents"),
|
||||
path.join(".claude", "memory"),
|
||||
path.join(".claude", "commands"),
|
||||
".codex",
|
||||
path.join(".codex", "memories"),
|
||||
path.join(".codex", "skills"),
|
||||
".cursor",
|
||||
".ssh",
|
||||
".docker",
|
||||
|
|
@ -92,22 +106,29 @@ for (const dir of [
|
|||
".azure",
|
||||
path.join(".config", "gh"),
|
||||
path.join(".config", "git"),
|
||||
]) {
|
||||
];
|
||||
for (const dir of dirs) {
|
||||
if (fs.existsSync(path.join(home, dir))) {
|
||||
continue;
|
||||
}
|
||||
fs.mkdirSync(path.join(home, dir), { recursive: true });
|
||||
}
|
||||
|
||||
// Claude Code reads onboarding state (`hasCompletedOnboarding`, `userID`,
|
||||
// per-project trust) from `~/.claude.json` — a FILE at $HOME, separate
|
||||
// from the `~/.claude/` directory. devcontainer.json bind-mounts this
|
||||
// read-only at /host/.claude.json so post-create.sh can seed the
|
||||
// container's `~/.claude.json` and skip the onboarding wizard. Make sure
|
||||
// the file exists on the host first (Docker rejects bind mounts with a
|
||||
// missing source).
|
||||
const claudeJson = path.join(home, ".claude.json");
|
||||
if (!fs.existsSync(claudeJson)) {
|
||||
fs.closeSync(fs.openSync(claudeJson, "a"));
|
||||
// File bind-mount sources. devcontainer.json bind-mounts each file
|
||||
// individually (so the host file IS the container file — bidirectional
|
||||
// share). Touch-empty if absent so Docker doesn't reject the mount.
|
||||
// `~/.claude.json` carries `hasCompletedOnboarding` + MCP user-scope +
|
||||
// per-project trust; `~/.claude/settings.json` carries theme + enabled
|
||||
// plugins; `~/.codex/config.toml` carries Codex user prefs.
|
||||
const files = [
|
||||
".claude.json",
|
||||
path.join(".claude", "settings.json"),
|
||||
path.join(".codex", "config.toml"),
|
||||
];
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(home, file);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
fs.closeSync(fs.openSync(fullPath, "a"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,59 +29,27 @@ sudo chown -R node:node \
|
|||
/home/node/.cursor \
|
||||
/commandhistory
|
||||
|
||||
echo "[post-create] 2/7: stage AI CLI config (read-only host share + per-container credentials)"
|
||||
# Host's ~/.claude / ~/.codex / ~/.cursor are bind-mounted READ-ONLY at
|
||||
# /host/.<cli>. The container's actual config dirs are per-devcontainer
|
||||
# named volumes at /home/node/.<cli>. We selectively SYMLINK shareable
|
||||
# subdirs (plugins, skills, agents, memory, commands) from /host into the
|
||||
# named volume so installing a plugin on the host lets the container see
|
||||
# it on next rebuild. Read-only mount means container code can't write
|
||||
# back — a compromised npm dep can't drop a malicious agent / skill /
|
||||
# plugin into the host config that the next host Claude session would
|
||||
# autoload. We deliberately do NOT symlink `ide/` (lock-file PID
|
||||
# collisions across host/container Claude Code instances), `projects/`
|
||||
# (host and container encode the workspace path differently — host
|
||||
# `D--development-coding-GitNexus` vs container `-workspace` — and
|
||||
# bidirectional writes split memory across two ghost project dirs), or
|
||||
# `settings.json` (container CLI is version-pinned while host floats;
|
||||
# bidirectional writes cause silent schema drift). Those stay container-
|
||||
# local in the named volume.
|
||||
echo "[post-create] 2/7: sync AI CLI credentials + identity from host"
|
||||
# Plugins, skills, agents, memory, commands, settings.json, $HOME/.claude.json,
|
||||
# Codex config.toml/memories/skills are all RW bind-mounted directly from
|
||||
# host in devcontainer.json — they live on host and reads/writes go
|
||||
# bidirectionally. Nothing for this script to do for those.
|
||||
#
|
||||
# CREDENTIALS (.credentials.json / auth.json / cli-config.json) and
|
||||
# `.claude.json` (the onboarding-state file at $HOME) are COPIED on
|
||||
# first run, not symlinked. The container then manages its own refresh
|
||||
# in the named volume; the host's copies are untouched. Refresh-token
|
||||
# divergence is real (Anthropic rotates on every use), so an unattended
|
||||
# container session can hit a silent 401 if the host has refreshed since
|
||||
# the copy — re-run `claude login` inside the container to refresh.
|
||||
|
||||
link_readonly_share() {
|
||||
local src_root=$1
|
||||
local dst_root=$2
|
||||
shift 2
|
||||
for name in "$@"; do
|
||||
# If dst exists as a non-symlink (stale from a prior run), remove
|
||||
# it first so the symlink can land. Skips if dst is already the
|
||||
# right symlink.
|
||||
if [ -e "$src_root/$name" ]; then
|
||||
if [ -L "$dst_root/$name" ]; then
|
||||
continue # already a symlink — leave alone
|
||||
fi
|
||||
rm -rf "$dst_root/$name"
|
||||
ln -s "$src_root/$name" "$dst_root/$name"
|
||||
fi
|
||||
done
|
||||
}
|
||||
# What stays per-container (in the named volume) and gets SYNCED from
|
||||
# host on container-create:
|
||||
# - .credentials.json (Claude OAuth tokens)
|
||||
# - .claude/.claude.json (Claude identity: userID, oauthAccount,
|
||||
# migration tracking — different file from $HOME/.claude.json)
|
||||
# - auth.json (Codex)
|
||||
# - cli-config.json (Cursor — conflates auth + settings)
|
||||
#
|
||||
# Sync semantics: ALWAYS overwrite from host on container-create, so a
|
||||
# fresh container starts logged in as host's user (if host had creds).
|
||||
# Container manages its own refresh from there until next rebuild.
|
||||
# Logging out in container doesn't affect host. Per-container login is
|
||||
# the design goal; bind-mounting these would make logout shared.
|
||||
|
||||
sync_from_host() {
|
||||
# ALWAYS overwrite from host on container-create, so a fresh container
|
||||
# starts in the same auth/trust state as the host. Guarding on
|
||||
# "[ ! -e $dst ]" was a bug: stale named volumes left over from earlier
|
||||
# builds (or from a previous container instance that wrote interim
|
||||
# state) made the guard return false and skip the copy, leaving the
|
||||
# container desynced from the host. Container can still mutate the
|
||||
# files after this point — divergence is reset only on container
|
||||
# rebuild (which is when `post-create.sh` runs).
|
||||
local src=$1
|
||||
local dst=$2
|
||||
local mode=${3:-600}
|
||||
|
|
@ -92,57 +60,21 @@ sync_from_host() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Claude Code — share the user-installed surface (plugins/skills/agents/
|
||||
# memory/commands) read-only from host. Skip ide/projects per above.
|
||||
link_readonly_share /host/.claude /home/node/.claude \
|
||||
plugins skills agents memory commands
|
||||
|
||||
# State files. Claude splits state across two files: $HOME/.claude.json
|
||||
# (hasCompletedOnboarding, MCP user-scope config, project trust,
|
||||
# tipsHistory) and $CLAUDE_CONFIG_DIR/.claude.json (userID, oauthAccount,
|
||||
# migration tracking). With CLAUDE_CONFIG_DIR unset (see devcontainer.json
|
||||
# rationale), Claude reads onboarding state from $HOME/.claude.json —
|
||||
# which carries `hasCompletedOnboarding` and skips the wizard.
|
||||
sync_from_host /host/.claude.json /home/node/.claude.json 644
|
||||
sync_from_host /host/.claude/.claude.json /home/node/.claude/.claude.json 644
|
||||
|
||||
# settings.json carries the active theme, enabled plugins, and known
|
||||
# marketplaces — without this, theme picker fires on every fresh volume
|
||||
# and host-installed plugins stay disabled even though their files are
|
||||
# symlinked in by link_readonly_share above. We pin Claude Code version
|
||||
# in devcontainer.json, so schema drift between host (floating) and
|
||||
# container (pinned) is bounded — Claude tolerates unknown keys, and
|
||||
# we re-sync on every container-create.
|
||||
sync_from_host /host/.claude/settings.json /home/node/.claude/settings.json 644
|
||||
|
||||
# Stub fallback at $HOME/.claude.json for first-time hosts that never
|
||||
# ran Claude Code (host file is empty or missing — `sync_from_host`
|
||||
# leaves the container's path missing too).
|
||||
if [ ! -f /home/node/.claude.json ]; then
|
||||
echo '{"hasCompletedOnboarding":true,"installMethod":"global"}' \
|
||||
> /home/node/.claude.json
|
||||
chmod 644 /home/node/.claude.json
|
||||
fi
|
||||
|
||||
# Credentials. Container manages its own refresh until next rebuild.
|
||||
sync_from_host \
|
||||
/host/.claude/.credentials.json /home/node/.claude/.credentials.json
|
||||
sync_from_host \
|
||||
/host/.claude/.claude.json /home/node/.claude/.claude.json 644
|
||||
|
||||
# Codex — share config.toml + memories/ + skills/ (the user-installed
|
||||
# surface, symmetric with Claude's plugin/skill/agent sharing). Sync
|
||||
# auth.json on container-create. Hosts using OS keyring storage
|
||||
# Codex auth. Hosts using OS keyring storage
|
||||
# (`cli_auth_credentials_store = "keyring"`, default on macOS) have no
|
||||
# auth.json on disk — the copy silently no-ops and
|
||||
# `codex login --device-auth` inside the container is the path.
|
||||
link_readonly_share /host/.codex /home/node/.codex \
|
||||
config.toml memories skills
|
||||
sync_from_host \
|
||||
/host/.codex/auth.json /home/node/.codex/auth.json
|
||||
|
||||
# Cursor CLI — cli-config.json conflates auth + settings, no shareable
|
||||
# subdirs. Copy on container create. Cursor has known upstream issues
|
||||
# authenticating inside Docker even with correctly-copied config; if
|
||||
# `cursor-agent` reports auth errors after copy, re-run
|
||||
# Cursor CLI — cli-config.json conflates auth + settings. Cursor has known
|
||||
# upstream issues authenticating inside Docker even with correctly-copied
|
||||
# config; if `cursor-agent` reports auth errors after copy, re-run
|
||||
# `cursor-agent login` inside the container.
|
||||
sync_from_host \
|
||||
/host/.cursor/cli-config.json /home/node/.cursor/cli-config.json
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue