diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5d308a169..3b3009dc7 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -108,17 +108,30 @@ // `/Users/x/...` on macOS) so it CANNOT be shared as-is — stays in // the named volume, generated by post-create.sh from host's versions // with paths translated to the container's Linux paths. + // DIRECTORY binds — bidirectional, atomic writes inside the dir work + // fine (same filesystem). Sub-path writes from container go to host + // immediately; host changes are visible to container immediately. "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", - "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", + // + // SINGLE-FILE binds for settings.json / .claude.json / config.toml + // are deliberately ABSENT. On Docker Desktop Windows the named-volume + // is ext4 (/dev/sdd) and a single-file bind from host is 9p drvfs — + // different filesystems. Atomic config writes (write `foo.tmp` -> + // rename onto `foo`) trip EXDEV and fail with `Device or resource + // busy` / `inter-device move failed`. Codex's TUI hits this as + // "config/batchWrite failed in TUI"; Claude silently loses writes + // through the same mechanism. Instead: host stage at /host/.claude + // (RO) + post-create.sh copies these files into the named volume on + // every container-create. Host changes propagate on rebuild; + // container changes stay container-local until 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", "source=${localEnv:HOME}/.config/gh,target=/home/node/.config/gh,type=bind", diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 892e1ffbe..533ebae6f 100644 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -73,6 +73,16 @@ sync_from_host \ sync_from_host \ /host/.claude/.claude.json /home/node/.claude/.claude.json 644 +# State files that USED to be single-file bind mounts but couldn't be: on +# Docker Desktop Windows the named volume (ext4) and the host bind-mount +# (9p drvfs) are different filesystems, so atomic config writes +# (`tmp -> rename onto target`) trip EXDEV / Device-or-resource-busy. +# Copy host's version into the named volume on container-create; container +# can rewrite freely from there until next rebuild resyncs. +sync_from_host /host/.claude/settings.json /home/node/.claude/settings.json 644 +sync_from_host /host/.claude.json /home/node/.claude.json 644 +sync_from_host /host/.codex/config.toml /home/node/.codex/config.toml 644 + # Plugin registry path translation. Claude writes absolute OS-native paths # into known_marketplaces.json (`installLocation`), installed_plugins.json # (`installPath`), and plugin-catalog-cache.json — `C:\Users\X\.claude\...`