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
Patterns for setting up Node.js development in a secured DevContainer.
Install Node.js and pnpm globally (as root) before switching to the non-root user. This is simpler and safer than userspace installation.
FROM debian:trixie
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Install system dependencies (sudo intentionally omitted for security)
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
git \
xz-utils \
jq \
vim \
ripgrep \
fd-find \
docker-cli \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user with host UID/GID for file permission compatibility
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID
RUN if getent group $USER_GID >/dev/null; then \
useradd --uid $USER_UID --gid $USER_GID -m $USERNAME; \
else \
groupadd --gid $USER_GID $USERNAME && \
useradd --uid $USER_UID --gid $USER_GID -m $USERNAME; \
fi
RUN mkdir -p /app/node_modules && chown -R $USER_UID:$USER_GID /app
# Install Node.js — pin version via .nvmrc, detect architecture automatically
COPY .nvmrc /tmp/.nvmrc
RUN NODE_VERSION=$(cat /tmp/.nvmrc | tr -d '[:space:]') \
&& ARCH=$(uname -m | sed 's/x86_64/x64/' | sed 's/aarch64/arm64/') \
&& curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${ARCH}.tar.xz" \
| tar -xJ -C /usr/local --strip-components=1 \
&& rm /tmp/.nvmrc \
&& npm install -g pnpm@10.12.4
# Switch to non-root user
USER $USERNAME
# Set up shell environment
ENV SHELL=/bin/bash
# Security hardening (see SECURITY-HARDENING.md)
RUN mkdir -p /home/vscode/.config && cat << 'HARDEN' > /home/vscode/.config/security-harden.sh
unset VSCODE_IPC_HOOK_CLI VSCODE_GIT_IPC_HANDLE GIT_ASKPASS \
VSCODE_GIT_ASKPASS_MAIN VSCODE_GIT_ASKPASS_NODE VSCODE_GIT_ASKPASS_EXTRA_ARGS \
REMOTE_CONTAINERS_IPC REMOTE_CONTAINERS_SOCKETS REMOTE_CONTAINERS_DISPLAY_SOCK \
WAYLAND_DISPLAY
export BROWSER= SSH_AUTH_SOCK= GPG_AGENT_INFO=
HARDEN
RUN sed -i '1i source ~/.config/security-harden.sh 2>/dev/null || true' ~/.bashrc \
&& sed -i '2i export PATH="$HOME/.local/bin:$PATH"' ~/.bashrc \
&& mkdir -p /home/vscode/.local/bin /home/vscode/.cache \
/home/vscode/.local/share/pnpm/store /home/vscode/.local/share/pnpm/global
WORKDIR /app
CMD ["sleep", "infinity"]node:lts?.nvmrc (single source of truth)Userspace installation (--prefix /home/vscode/.local) adds complexity:
chown of the install directoryPNPM_HOME and extra PATH entriespnpm config set global-bin-dir at runtimeGlobal installation is simpler — pnpm goes into /usr/local/bin/ alongside Node.
Some things should happen at container start rather than build time, to stay fresh without rebuilding the image:
command: >
bash -c '. /home/vscode/.bashrc &&
curl -fsSL https://claude.ai/install.sh | bash &&
pnpm config set store-dir /home/vscode/.local/share/pnpm/store &&
pnpm install;
sleep infinity'Why at startup?
claude CLI — always get the latest versionpnpm install — dependencies change frequentlyWhy sleep infinity?
The container must stay running for VS Code to attach. The semicolon before sleep (not &&) ensures the container stays up even if setup fails.
volumes:
# Workspace — host-mounted for live editing
- ..:/app:cached
# node_modules — isolated Docker volumes (not synced to host)
# Prevents OS-specific native module issues and speeds up file I/O
- node-modules:/app/node_modules
# Shared pnpm store — mounted from host for cross-worktree cache hits
- ${PNPM_STORE_PATH}:/home/vscode/.local/share/pnpm/store:cachedclaude-code-${WORKTREE_NAME}-node-modules) keep each worktree's dependencies separateTo avoid file permission issues between the container and host-mounted workspace:
build:
args:
USER_UID: ${HOST_UID:-1000}
USER_GID: ${HOST_GID:-1000}Generate these in initializeCommand (runs on host):
{
"initializeCommand": "bash -c 'echo \"HOST_UID=$(id -u)\" > .devcontainer/.env && echo \"HOST_GID=$(id -g)\" >> .devcontainer/.env'"
}For proper Unicode handling (important for some npm packages and git):
RUN apt-get install -y locales \
&& sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen \
&& locale-gen
ENV LANG=en_US.UTF-8
ENV LC_ALL=en_US.UTF-8