diff --git a/.devcontainer/README.md b/.devcontainer/README.md index c898676a7..6978f2932 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -78,17 +78,30 @@ Same as macOS — open in VS Code and reopen in container. `updateRemoteUserUID: The following directories inside the container are **bind-mounted directly from your host's `$HOME`**: -| Container path | Host source | Mode | -|---|---|---| -| `~/.claude` | `$HOME/.claude` | read-write | -| `~/.codex` | `$HOME/.codex` | read-write | -| `~/.cursor` | `$HOME/.cursor` | read-write | -| `~/.config/git` | `$HOME/.config/git` | **read-only** | -| `~/.ssh` | `$HOME/.ssh` | **read-only** | -| `~/.config/gh` | `$HOME/.config/gh` | read-write | +| Container path | Host source | Mode | Why | +|---|---|---|---| +| `~/.claude` | `$HOME/.claude` | read-write | Claude Code plugins, skills, agents, memory, settings, OAuth | +| `~/.codex` | `$HOME/.codex` | read-write | Codex CLI auth + config + profiles | +| `~/.cursor` | `$HOME/.cursor` | read-write | Cursor CLI auth + rules + cli-config | +| `~/.config/git` | `$HOME/.config/git` | **read-only** | XDG-style git config / ignore / attributes | +| `~/.ssh` | `$HOME/.ssh` | **read-only** | SSH commit signing + git push over SSH | +| `~/.config/gh` | `$HOME/.config/gh` | read-write | `gh` CLI auth (PR create, issue create, checks) | +| `~/.docker` | `$HOME/.docker` | read-write | Container registry auth (`docker push` to ghcr/dockerhub if you add docker-in-docker) + buildx config | +| `~/.aws` | `$HOME/.aws` | **read-only** | AWS CLI / SDK credentials (forward-compat — empty by default) | +| `~/.azure` | `$HOME/.azure` | **read-only** | Azure CLI credentials (forward-compat — empty by default) | `~/.gitconfig` is **not** bind-mounted — VS Code's Dev Containers extension auto-copies the host's gitconfig into the container at attach time (this is built-in behavior, not something this devcontainer configures). The bind-mount approach conflicts with that auto-copy mechanism, so we let VS Code own it. The end result is the same: your host's `user.name` / `user.email` are available inside the container. +If a host source dir doesn't exist when the container is first created, the `initializeCommand` (`node .devcontainer/ensure-host-config-dirs.cjs`) creates it empty — so the bind mount always has a valid source, and the cloud configs you don't use yet are ready when you do. + +### What you still don't have inside the container + +These are commonly-needed CLIs that aren't installed by default — adding them would be follow-up work, not in this PR's scope: + +- **Docker CLI** (for `docker push` / `docker build` from inside the container). Add via `ghcr.io/devcontainers/features/docker-outside-of-docker:1` to the `features` block — `~/.docker/` is already mounted so `docker login` state from your host will work immediately. +- **AWS CLI / Azure CLI / gcloud / kubectl** — same pattern: add the matching Feature, the host config dirs already flow through. +- **Private npm registry auth** (`~/.npmrc`) — you don't have a global one on this host. If you ever start using private packages, add `source=${localEnv:HOME}/.npmrc,target=/home/node/.npmrc,type=bind,readonly` to the mounts. + That means: - **Authentication is shared.** If you're already logged in on the host (`claude login`, `codex login`, `cursor-agent login`, `gh auth login`), you're already logged in inside the container. No second login step. @@ -102,7 +115,16 @@ The bind mount source directories are guaranteed to exist by the `initializeComm ### Trust boundary, concretely -Host and container share a single trust boundary by design — fine for personal-dev, but the consequence is concrete: any malicious npm package or `postinstall` script in the workspace dep tree, running inside the container with these bind mounts active, has direct read access to your OAuth refresh tokens for all three CLIs, your `gh` token, **your SSH private keys under `~/.ssh/`**, and `~/.claude/projects//memory/MEMORY.md` (which may contain user-stored secrets if you've used the `/remember` skill). Read-only mounts on `~/.ssh`, `~/.gitconfig`, and `~/.config/git` prevent container code from modifying or deleting them, but they're still readable. The egress firewall is deferred (see "What's not included (yet)" below) so a compromised package would also have unrestricted network to exfiltrate. +Host and container share a single trust boundary by design — fine for personal-dev, but the consequence is concrete. Any malicious npm package or `postinstall` script in the workspace dep tree, running inside the container with these bind mounts active, has direct read access to: + +- OAuth refresh tokens for **Claude Code, Codex, Cursor** (under `~/.claude`, `~/.codex`, `~/.cursor`) +- Your **`gh` token** (`~/.config/gh`) +- Your **SSH private keys** (`~/.ssh/`) +- Docker registry tokens in **`~/.docker/config.json`** (registry passwords/PATs for ghcr / dockerhub if you've `docker login`-ed) +- AWS/Azure CLI credentials if you've populated `~/.aws/` or `~/.azure/` +- `~/.claude/projects//memory/MEMORY.md` (which may contain user-stored secrets if you've used the `/remember` skill) + +Read-only mounts on `~/.ssh`, `~/.config/git`, `~/.aws`, and `~/.azure` prevent container code from modifying or deleting them, but they're still readable. The egress firewall is deferred (see "What's not included (yet)" below) so a compromised package would also have unrestricted network to exfiltrate. **If a workspace dep is ever found compromised**, rotate credentials at the vendor side — local file deletion is insufficient because tokens may have already left: diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 36d06c987..93f93552d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -69,6 +69,9 @@ "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", + "source=${localEnv:HOME}/.docker,target=/home/node/.docker,type=bind", + "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", diff --git a/.devcontainer/ensure-host-config-dirs.cjs b/.devcontainer/ensure-host-config-dirs.cjs index 285f1d465..27a5c238c 100644 --- a/.devcontainer/ensure-host-config-dirs.cjs +++ b/.devcontainer/ensure-host-config-dirs.cjs @@ -87,6 +87,9 @@ for (const dir of [ ".codex", ".cursor", ".ssh", + ".docker", + ".aws", + ".azure", path.join(".config", "gh"), path.join(".config", "git"), ]) {