feat(devcontainer): support Windows-native via auto setx HOME on first run

Reverses the "WSL2 required on Windows" posture. Windows-native now
works after a one-time auto-handled setup.

The root cause of the bind-mount failure: VS Code resolves
`${localEnv:HOME}` by reading its own process env, and Windows doesn't
set `HOME` by default — Windows uses `USERPROFILE`. So the bind sources
were collapsing to `/.claude`, `/.codex`, etc., and Docker rejected them.

`ensure-host-config-dirs.cjs` now handles this automatically on Windows
hosts where `HOME` is unset:

1. Runs `setx HOME "%USERPROFILE%"`, which writes to the user-level
   Windows environment (HKCU\Environment) — no admin required. Every
   future user process inherits HOME from there.
2. Prints a clear one-time setup banner explaining the user needs to
   fully restart VS Code (File > Exit, not just close the window) for
   VS Code to pick up the new env at its next startup.
3. Exits 1 so VS Code surfaces this as a clean container-create failure
   instead of letting Docker error opaquely later.

On the second Reopen-in-Container attempt, `HOME` is now set in VS
Code's env, the script skips the setup block, creates the bind-mount
source dirs, and the container builds normally. Subsequent rebuilds
have no extra steps.

Mac, Linux, and WSL2 hosts have `HOME` set by the shell, so the new
block is a no-op there. Same `devcontainer.json` works across all
supported hosts.

README rewritten to reflect the new posture:

- Header lists Windows 11 (native) as a supported host alongside macOS,
  Linux, and WSL2, with a note that Windows-native gets a one-time
  HOME setup handled by the initializeCommand.
- New "Windows 11 setup" section walks through the auto-handled setup
  flow + a manual `setx HOME "%USERPROFILE%"` fallback for users who
  want to do it themselves.
- "Known trade-offs of Windows-native vs WSL2" subsection lays out the
  Docker Desktop Windows bind-mount edge cases (file watchers, npm
  install perf, husky/_ EPERM) so users opting into Windows-native do
  so eyes-open. WSL2 remains documented as the faster path for users
  who want it, but it's no longer the only supported one.
- Troubleshooting table gets two new rows: the one-time setup banner
  (with "what to do" instructions) and the residual `bind source path
  does not exist` case (run setx manually + fully exit VS Code).
This commit is contained in:
Gergo Magyar 2026-05-28 14:08:45 +01:00
parent f4dc20289a
commit a29a1cfb9f
2 changed files with 92 additions and 37 deletions

View file

@ -1,6 +1,6 @@
# GitNexus Devcontainer
A cross-platform Dev Container that pre-installs Claude Code, OpenAI Codex CLI, and Cursor CLI alongside the GitNexus native build chain. Supported hosts: **macOS, Linux, and Windows 11 via WSL2** (Windows-native is unsupported — the bind mounts require `$HOME` to be set on the host shell, which cmd.exe on Windows-native lacks).
A cross-platform Dev Container that pre-installs Claude Code, OpenAI Codex CLI, and Cursor CLI alongside the GitNexus native build chain. Supported hosts: **macOS, Linux, Windows 11 (native), and Windows 11 via WSL2.** Windows-native needs a **one-time `HOME` env var setup** — handled automatically by the `initializeCommand` on first run (see [Windows 11 setup](#windows-11-setup)).
## Quick start
@ -11,11 +11,38 @@ A cross-platform Dev Container that pre-installs Claude Code, OpenAI Codex CLI,
5. Wait for the first build (~3–6 minutes) and `postCreateCommand` to finish installing workspace dependencies.
6. Authenticate the three CLIs once — see [First-time CLI authentication](#first-time-cli-authentication) below.
## Windows 11 — WSL2 is required
## Windows 11 setup
**Windows-native is unsupported.** The host bind mounts use `${localEnv:HOME}/.claude` (and `.codex`, `.cursor`, `.ssh`, `.config/git`, `.config/gh`, `.gitconfig`). VS Code resolves `${localEnv:HOME}` by reading the host shell's `HOME` env var — and cmd.exe on Windows-native has no `HOME` set. The bind sources then collapse to filesystem-root paths (`/.claude`, `/.codex`, …) and Docker rejects them with `bind source path does not exist`. The `initializeCommand`'s Node script detects this case and fails fast with a pointer at this section instead of letting Docker error opaquely.
### Windows-native (one-time setup, then "just works")
Beyond the `HOME` resolution issue, WSL2 also gives you reliable file watchers (Vite/jest `--watch` work), 3-5× faster `npm install` IO, and avoids the Docker Desktop Windows bind-mount permission edge cases (e.g., the husky `.husky/_/h` EPERM class).
The host bind mounts use `${localEnv:HOME}/.claude` (and `.codex`, `.cursor`, `.ssh`, `.config/git`, `.config/gh`, `.gitconfig`). VS Code resolves `${localEnv:HOME}` by reading its own process env, and Windows doesn't set `HOME` by default — it uses `USERPROFILE`. So the bind mounts can't resolve until you tell Windows to also expose your profile as `HOME`.
The `initializeCommand` (`node .devcontainer/ensure-host-config-dirs.cjs`) handles this automatically:
1. **First time you Reopen in Container**, the script detects the missing `HOME`, runs `setx HOME "%USERPROFILE%"` (which writes to your user-level Windows env — no admin needed), prints a one-time setup banner, and exits.
2. **Close all VS Code windows** (File → Exit) and reopen. VS Code picks up the new `HOME` at startup.
3. **Reopen in Container again.** The script now sees `HOME=C:\Users\<you>`, skips the setup block, creates the bind-mount source dirs, and Docker brings the container up.
Subsequent rebuilds work normally with no extra steps. The `HOME` env var is set persistently in your Windows user environment, so it'll be there for every future VS Code session (and any other tool that wants `HOME`).
If you'd rather set it manually before opening the container:
```powershell
setx HOME "%USERPROFILE%"
# Close & reopen VS Code
```
### Known trade-offs of Windows-native vs WSL2
Windows-native works, but Docker Desktop's Windows bind-mount layer has rough edges that WSL2 avoids:
- **File watchers can miss events.** Vite / jest `--watch` running inside the container watching workspace files mounted from `D:\...` may miss changes — chokidar polling (`CHOKIDAR_USEPOLLING=true`) is the usual workaround.
- **`npm install` is 3-5× slower** through the Windows-to-Linux bind-mount translation than on a WSL2-native filesystem.
- **Permission edge cases.** The husky `.husky/_/h` EPERM class we hit earlier in this PR is specific to Windows-side bind mounts changing UID ownership between container runs. `post-create.sh` clears the cache defensively to keep this from being fatal, but it's still a real source of friction.
If you hit any of those and want to migrate to WSL2 later, the steps are below.
### WSL2 (faster, fewer edge cases)
To clone and open the repo inside WSL2:
@ -187,7 +214,8 @@ Bump `CLAUDE_CODE_VERSION` and `CODEX_VERSION` in `.devcontainer/devcontainer.js
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `ERROR: GitNexus devcontainer requires WSL2 on Windows 11` from `initializeCommand` | You opened the repo from a Windows-native path; `cmd.exe` has no `$HOME`, so the bind mounts can't resolve | Clone the repo inside WSL2 and reopen — see [Windows 11 — WSL2 is required](#windows-11--wsl2-is-required) |
| `GitNexus devcontainer one-time Windows setup` banner from `initializeCommand` | First-time Windows-native Reopen-in-Container; `HOME` env var was missing | The script just ran `setx HOME "%USERPROFILE%"` for you. Close ALL VS Code windows (File → Exit) and reopen — see [Windows 11 setup](#windows-11-setup) |
| `bind source path does not exist: /.claude` (or similar) from Docker | Windows-native `HOME` env var is still missing even after one rebuild — `setx` may have failed or VS Code wasn't fully restarted | Run `setx HOME "%USERPROFILE%"` in a Windows shell manually, fully exit VS Code (check Task Manager that no `Code.exe` remains), reopen |
| `EACCES` / `EPERM` writing into `~/.claude`, `~/.codex`, or `~/.cursor` inside the container | Stale state from a previous container with a different effective UID | Move the affected dir aside and let the CLI rebuild it (`mv ~/.claude ~/.claude.bak` and log in again). Long-term: WSL2 setup, which doesn't hit this class of issue |
| `EPERM: operation not permitted, copyfile ... '.husky/_/h'` in `postCreateCommand` | Leftover `.husky/_/` from a previous container run on a Windows-side bind mount | `post-create.sh` already runs `rm -rf .husky/_` defensively. If you hit this on an older config, delete `.husky/_/` on the host and rebuild. Long-term: clone in WSL2 |
| Vite never hot-reloads | Repo cloned on Windows side, not WSL2 | Re-clone inside WSL2 |

View file

@ -16,39 +16,66 @@ const fs = require("fs");
const os = require("os");
const path = require("path");
// Fail-fast on Windows-native (no $HOME set). VS Code resolves the bind
// mount sources via `${localEnv:HOME}` reading the host shell env, and
// cmd.exe on Windows has no HOME variable — so bind sources collapse to
// `/.claude`, `/.codex`, etc., and Docker rejects them with
// `bind source path does not exist`. Surface the actual root cause here
// instead of letting Docker error opaquely later.
// Windows-native auto-setup. VS Code resolves the bind-mount sources via
// `${localEnv:HOME}` reading its own process env, and Windows doesn't set
// `HOME` by default (it uses `USERPROFILE`). Without `HOME`, the bind
// sources collapse to filesystem-root paths (`/.claude`, `/.codex`, ...)
// and Docker rejects them with `bind source path does not exist`.
//
// Fix: persist `HOME=%USERPROFILE%` to the user's environment via `setx`.
// `setx` writes to `HKCU\Environment` and the new value is inherited by
// every process the user launches after — including VS Code after a
// restart. The current VS Code process can't see the update (its env was
// fixed at launch), so we instruct the user to restart VS Code once.
//
// Subsequent runs detect `HOME` is set, skip this block, and proceed
// normally. Mac/Linux/WSL hosts have `HOME` set by the shell, so this
// block is a no-op on those platforms.
if (process.platform === "win32" && !process.env.HOME) {
console.error("ERROR: GitNexus devcontainer requires WSL2 on Windows 11.");
console.error("");
console.error(
"You opened this from a Windows-native path. The host bind mounts use",
);
console.error(
"`${localEnv:HOME}/.claude` etc., which VS Code resolves from the host shell",
);
console.error(
"env. cmd.exe on Windows has no HOME variable, so the bind sources resolve",
);
console.error(
"to filesystem-root paths (/.claude, /.codex, ...) and Docker rejects them.",
);
console.error("");
console.error("Clone the repo inside WSL2 and re-open from there:");
console.error(" wsl");
console.error(
" cd ~ && git clone https://github.com/abhigyanpatwari/GitNexus.git",
);
console.error(" cd GitNexus && code .");
console.error("");
console.error(
"See .devcontainer/README.md - Windows 11 - WSL2 is required.",
);
process.exit(1);
const userprofile = process.env.USERPROFILE;
if (userprofile) {
try {
require("child_process").execFileSync("setx", ["HOME", userprofile], {
stdio: "ignore",
});
console.error("");
console.error("=".repeat(70));
console.error(" GitNexus devcontainer one-time Windows setup");
console.error("=".repeat(70));
console.error("");
console.error(`HOME has been set to %USERPROFILE% (${userprofile}).`);
console.error(
"VS Code reads this at startup, so the current session can't pick it up.",
);
console.error("");
console.error(
" 1. Close ALL VS Code windows (File > Exit, not just the window).",
);
console.error(
" 2. Reopen VS Code, open this folder, and re-run Reopen in Container.",
);
console.error("");
console.error(
"This is a one-time setup. Subsequent rebuilds work normally.",
);
console.error("=".repeat(70));
process.exit(1);
} catch (err) {
console.error("ERROR: failed to set HOME automatically: " + err.message);
console.error("");
console.error("Run this in a Windows shell, then restart VS Code:");
console.error(' setx HOME "%USERPROFILE%"');
process.exit(1);
}
} else {
console.error("ERROR: neither HOME nor USERPROFILE is set on this host.");
console.error("");
console.error(
"Set HOME to your user profile directory and restart VS Code:",
);
console.error(' setx HOME "%USERPROFILE%"');
process.exit(1);
}
}
const home = os.homedir();