Guide for setting up secured VS Code dev containers for coding agents. Use when creating or hardening a DevContainer to sandbox Claude Code or other coding agents, configuring Docker socket proxies, handling VS Code IPC escape vectors, setting up git worktree support, or verifying security controls. Covers threat model, three-layer defence architecture, Node.js/pnpm setup, and verification testing.
95
95%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
Run multiple git worktrees simultaneously, each with its own isolated dev container sharing infrastructure services.
Git worktrees let you check out multiple branches as separate directories. Combined with DevContainers, each worktree gets its own container with isolated node_modules, while sharing databases and other infrastructure.
project/ # main worktree
project-feature-branch/ # git worktree add ../project-feature-branch feature-branch
project-bugfix/ # git worktree add ../project-bugfix bugfix┌─────────────────────────────────────────────┐
│ Shared Infrastructure │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ postgres │ │ emulator │ │ gcs │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ Docker network: dev │
└─────────────────────────────────────────────┘
↑ ↑ ↑
┌───────┴──────┐ ┌──────┴───────┐ ┌────┴────────┐
│ claude-code │ │ claude-code │ │ claude-code │
│ -main │ │ -feature │ │ -bugfix │
│ (worktree) │ │ (worktree) │ │ (worktree) │
└──────────────┘ └──────────────┘ └─────────────┘The initializeCommand generates a .env file with the worktree name (runs on the host):
{
"initializeCommand": "bash -c 'mkdir -p .devcontainer && echo \"WORKTREE_NAME=$(basename \"$PWD\")\" > .devcontainer/.env && echo \"GIT_MAIN_REPO_PATH=$(realpath \"$(git rev-parse --git-common-dir 2>/dev/null)/..\" 2>/dev/null || echo \"$PWD\")\" >> .devcontainer/.env && echo \"LOCAL_WORKSPACE_FOLDER=$PWD\" >> .devcontainer/.env && echo \"HOST_HOME=$HOME\" >> .devcontainer/.env && echo \"HOST_UID=$(id -u)\" >> .devcontainer/.env && echo \"HOST_GID=$(id -g)\" >> .devcontainer/.env'"
}This generates .devcontainer/.env:
WORKTREE_NAME=project-feature-branch
GIT_MAIN_REPO_PATH=/Users/you/code/project
LOCAL_WORKSPACE_FOLDER=/Users/you/code/project-feature-branch
HOST_HOME=/Users/you
HOST_UID=501
HOST_GID=20Use ${WORKTREE_NAME} in docker-compose.yml for unique naming:
services:
app:
container_name: claude-code-${WORKTREE_NAME:-default}
volumes:
- ..:/app:cached
# Per-worktree isolated volumes
- node-modules:/app/node_modules
env_file:
- .env # The generated .env file
volumes:
node-modules:
name: claude-code-${WORKTREE_NAME:-default}-node-modulesEach worktree gets its own named volume, preventing conflicts.
Git worktrees have a .git file (not directory) pointing to the main repo's .git directory. The container needs access to both:
volumes:
# Workspace (the worktree itself)
- ..:/app:cached
# Main repo's .git — mount to same absolute path for git to find it
- ${GIT_MAIN_REPO_PATH}/.git:${GIT_MAIN_REPO_PATH}/.git:cachedWhy the same absolute path? The worktree's .git file contains an absolute path like gitdir: /Users/you/code/project/.git/worktrees/feature-branch. If we mount .git to a different path, git won't find it.
All worktree containers join the same Docker network to access shared infrastructure:
networks:
dev:
external: true # Created by docker-compose.shared.yml or manuallyStart shared infrastructure once:
docker compose -f docker-compose.shared.yml up -dThe initializeCommand can detect the OS and set the correct pnpm store path:
if [[ "$OSTYPE" == darwin* ]]; then
PNPM_STORE="$HOME/Library/pnpm/store"
else
PNPM_STORE="$HOME/.local/share/pnpm/store"
fi
echo "PNPM_STORE_PATH=$PNPM_STORE" >> .devcontainer/.envThen in docker-compose.yml:
volumes:
- ${PNPM_STORE_PATH}:/home/vscode/.local/share/pnpm/store:cachedAdd to .gitignore:
.devcontainer/.env
.claude-docker/Docker creates a directory on the host if a file mount target doesn't exist. Prevent this in initializeCommand:
mkdir -p .claude-docker
touch .claude-docker/.bash_history
[ -f .claude-docker/.claude.json ] || echo '{}' > .claude-docker/.claude.jsonThe .git directory mount path doesn't match the absolute path in the worktree's .git file. Ensure GIT_MAIN_REPO_PATH is correct and mounted to the same path.
If two worktrees share the same volume name (e.g. both called node-modules), they'll share state. Use ${WORKTREE_NAME} in volume names to isolate them.
If the dev container can't reach postgres or other services, ensure the shared infrastructure is started and the network exists:
docker network ls | grep dev
docker compose -f docker-compose.shared.yml up -d