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
Direct Docker socket access allows trivial container escape:
docker run -it --privileged --pid=host -v /:/host alpine chroot /hostThat's complete host access in one command.
Tecnativa docker-socket-proxy intercepts Docker API calls and blocks dangerous operations.
services:
docker-proxy:
image: tecnativa/docker-socket-proxy:latest
environment:
# Read-only operations - allowed
CONTAINERS: 1 # docker ps, logs, inspect
IMAGES: 1 # docker images
INFO: 1 # docker info
NETWORKS: 1 # docker network ls
VOLUMES: 1 # docker volume ls
# Dangerous operations - blocked
POST: 0 # No creating containers
BUILD: 0 # No building images
COMMIT: 0 # No committing containers
EXEC: 0 # No exec into containers
SWARM: 0 # No swarm operations
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- devThe dev container connects via the proxy, not the socket directly:
app:
environment:
DOCKER_HOST: tcp://docker-proxy:2375
depends_on:
- docker-proxyWhat the agent can do: docker ps, docker logs, docker inspect
What the agent cannot do: docker run, docker exec, docker build
Being able to view logs of sibling containers (databases, emulators) is genuinely useful for debugging. The proxy preserves this while blocking escape vectors.
Dev containers can communicate with sibling services over a shared Docker network. This is how you give the agent access to databases, emulators, etc. without mounting the Docker socket directly.
For teams using multiple worktrees or wanting to share infrastructure:
docker-compose.shared.yml (started separately, runs once):
services:
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: password
POSTGRES_DB: myapp-db
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- dev
# Add other shared services: Redis, emulators, etc.
networks:
dev:
name: dev
volumes:
pgdata:.devcontainer/docker-compose.yml (per worktree):
services:
docker-proxy:
# ... (as above)
networks:
- dev
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
volumes:
- ..:/app:cached
environment:
DOCKER_HOST: tcp://docker-proxy:2375
DATABASE_URL: postgresql://postgres:password@postgres:5432/myapp-db
networks:
- dev
depends_on:
- docker-proxy
command: sleep infinity
networks:
dev:
external: true # Created by docker-compose.shared.ymlKey points:
dev) must be created first (either by starting shared services or docker network create dev)postgres) not localhost for service connectionsWith EXEC: 0, docker exec is blocked. If the agent needs to trigger actions in other containers, expose HTTP endpoints:
db-admin:
image: your-db-admin
networks:
- dev
# Exposes HTTP endpoints for backup, migration, etc.This is better design anyway — explicit, logged, and rate-limitable.
If your app uses OAuth callbacks that require localhost, you can proxy ports from the dev container to the app container:
localhost-proxy:
image: alpine/socat
network_mode: "service:app"
entrypoint: ["/bin/sh", "-c"]
command:
- |
APP_HOST="${APP_CONTAINER_NAME:-app}"
for port in 3000 3001 3002 3003; do
socat TCP-LISTEN:$$port,fork,reuseaddr TCP:$$APP_HOST:3000 &
done
wait
restart: unless-stopped
depends_on:
- appThe Dockerfile should NOT install sudo:
# Install tools (sudo intentionally omitted for security)
RUN apt-get update && apt-get install -y --no-install-recommends \
git vim ripgrep docker-cli \
&& rm -rf /var/lib/apt/lists/*If the agent needs a tool, add it to the Dockerfile rather than giving it sudo access.
Don't mount ~/.ssh into the container. This blocks git push and prevents SSH key abuse:
volumes:
# Workspace
- ..:/app:cached
# Agent config only (no ~/.ssh mount!)
- ../.claude-docker/.claude.json:/home/vscode/.claude.jsonThe agent can still use all local git operations (commit, branch, stash, etc.) — it just can't push to remotes.