CtrlK
BlogDocsLog inGet started
Tessl Logo

daaain/devcontainer-security

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

Quality

95%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

SECURITY-HARDENING.md

VS Code IPC Security Hardening

VS Code's remote development model injects Unix sockets and environment variables into the container that can be abused for container escape. See The Red Guild's research for background.

Attack Surface

Sockets created in /tmp (and /tmp/user/<uid>/):

SocketPurposeAttack Vector
vscode-ssh-auth-*.sockSSH agent forwardingUse host SSH keys
vscode-ipc-*.sockCLI integrationExecute commands on host via code CLI
vscode-remote-containers-ipc-*.sockHost-container RPCExtension command execution
vscode-git-*.sockGit extension IPCGit credential access

Environment variables pointing to these sockets and host-side helper scripts:

VariableRisk
VSCODE_IPC_HOOK_CLIHost command execution via code CLI
VSCODE_GIT_IPC_HANDLEGit credential access via host
GIT_ASKPASS / VSCODE_GIT_ASKPASS_*HTTPS Git credential leakage
REMOTE_CONTAINERS_IPCExtension command execution
REMOTE_CONTAINERS_SOCKETSEnumerates multiple escape vectors
REMOTE_CONTAINERS_DISPLAY_SOCKGUI forwarding (low risk)
BROWSERHost-side execution via --openExternal
WAYLAND_DISPLAYGUI forwarding (low risk)

Three-Layer Defence

No single layer is sufficient. Together they cover each other's weaknesses.

Layer 1 — remoteEnv in devcontainer.json

First line of defence. Set variables to null (unset) or "" (empty).

{
  "remoteEnv": {
    "SSH_AUTH_SOCK": "",
    "GPG_AGENT_INFO": "",
    "BROWSER": "",
    "VSCODE_IPC_HOOK_CLI": null,
    "VSCODE_GIT_IPC_HANDLE": null,
    "GIT_ASKPASS": null,
    "VSCODE_GIT_ASKPASS_MAIN": null,
    "VSCODE_GIT_ASKPASS_NODE": null,
    "VSCODE_GIT_ASKPASS_EXTRA_ARGS": null,
    "REMOTE_CONTAINERS_IPC": null,
    "REMOTE_CONTAINERS_SOCKETS": null,
    "REMOTE_CONTAINERS_DISPLAY_SOCK": null,
    "WAYLAND_DISPLAY": null
  }
}

Limitation: VS Code re-injects several of these (BROWSER, VSCODE_IPC_HOOK_CLI, GIT_ASKPASS) when spawning new processes. This layer alone is insufficient.

Layer 2 — Shell Hardening Script (Primary Defence)

A script sourced from .bashrc that clears escape vector variables in every shell session.

Critical subtlety: Coding agents like Claude Code invoke bash as a non-interactive login shell. Bash sources ~/.profile~/.bashrc, but Debian's default .bashrc has an interactive guard:

case $- in
    *i*) ;;
      *) return;;  # <-- non-interactive shells exit here!
esac

The hardening script MUST be sourced before this guard (line 1 of .bashrc).

Create the script in the Dockerfile:

RUN mkdir -p /home/vscode/.config && cat << 'HARDEN' > /home/vscode/.config/security-harden.sh
# VS Code IPC sockets — can execute commands on the host
unset VSCODE_IPC_HOOK_CLI

# VS Code Git extension IPC — credential access via host
unset VSCODE_GIT_IPC_HANDLE \
      GIT_ASKPASS \
      VSCODE_GIT_ASKPASS_MAIN \
      VSCODE_GIT_ASKPASS_NODE \
      VSCODE_GIT_ASKPASS_EXTRA_ARGS

# Remote Containers extension IPC — host command execution bridge
unset REMOTE_CONTAINERS_IPC \
      REMOTE_CONTAINERS_SOCKETS \
      REMOTE_CONTAINERS_DISPLAY_SOCK

# GUI forwarding (low risk but unnecessary)
unset WAYLAND_DISPLAY

# Browser helper — can trigger actions on host via --openExternal
# Set to empty rather than unset to prevent fallback to defaults
export BROWSER=

# Agent forwarding — set to empty to prevent fallback to default socket paths
export SSH_AUTH_SOCK=
export GPG_AGENT_INFO=
HARDEN

# Source it BEFORE the interactive guard in .bashrc
RUN sed -i '1i source ~/.config/security-harden.sh 2>/dev/null || true' ~/.bashrc

Why unset vs export VAR=?

  • unset for IPC variables — nothing should try to use these
  • export VAR= for BROWSER, SSH_AUTH_SOCK, GPG_AGENT_INFO — prevents tools falling back to default socket paths

Layer 3 — Socket File Deletion (Defence in Depth)

Delete socket files so they can't be discovered via find. Two mechanisms handle different socket creation timings:

  • postStartCommand in devcontainer.json — runs before VS Code attaches, catches early sockets (SSH auth, remote-containers)
  • A background cleanup loop in the Docker Compose command — catches IPC and git sockets created during and after VS Code attach
{
  // In devcontainer.json — postStartCommand catches early sockets
  "postStartCommand": "find /tmp -maxdepth 2 \\( -name 'vscode-ssh-auth-*.sock' -o -name 'vscode-remote-containers-ipc-*.sock' -o -name 'vscode-remote-containers-*.js' \\) -delete 2>/dev/null || true"
}
# In docker-compose.yml — background loop catches IPC/git sockets.
# 10 passes at 30s intervals (~5 minutes) to catch all late-created sockets.
# Runs as a child of the container's own bash process, NOT a VS Code lifecycle command.
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 &&
  just platform-frontend playwright-ensure-browsers;
  (for i in 1 2 3 4 5 6 7 8 9 10; do sleep 30;
    find /tmp -maxdepth 2 \( -name "vscode-ipc-*.sock" -o -name "vscode-git-*.sock" \) -delete 2>/dev/null;
  done) &
  sleep infinity'

Note -maxdepth 2 — VS Code also creates sockets in /tmp/user/1000/.

Key discovery: vscode-ipc-*.sock and vscode-git-*.sock are not recreated after deletion — the IDE continues to work without them. However, VS Code creates IPC sockets at multiple times during startup — some 60+ seconds after attach. A single cleanup pass misses these late-created sockets. The 10-pass approach (every 30s for ~5 minutes) ensures all sockets are caught regardless of when VS Code creates them.

Why Docker Compose command instead of postAttachCommand? VS Code's postAttachCommand is unreliable for background processes. VS Code appears to use cgroup-based cleanup that kills ALL processes spawned during lifecycle commands, regardless of nohup, setsid, double-fork, or other daemonisation techniques. The Docker Compose command runs as the container's own process tree, which is not subject to VS Code's lifecycle management.

Docker Container Hardening

In addition to the VS Code IPC mitigations above, the container itself is hardened at the Docker level:

claude-code:
  cap_drop:
    - ALL
  security_opt:
    - no-new-privileges:true
  • cap_drop: [ALL] — Drops all Linux capabilities, reducing the kernel attack surface. The container only runs Node.js, git, and bash — none need special capabilities at runtime. If something breaks, specific capabilities can be added back with cap_add.
  • no-new-privileges:true — Prevents privilege escalation via setuid/setgid binaries. Combined with removing sudo, this ensures no process in the container can gain elevated privileges.

Together with the existing controls (no sudo, non-root user), these form Layer 1 hardening — strengthening the container boundary itself.

VS Code Settings to Disable

{
  "customizations": {
    "vscode": {
      "settings": {
        "dev.containers.dockerCredentialHelper": false,
        "dev.containers.copyGitConfig": false
      }
    }
  }
}
  • dockerCredentialHelper: false — prevents Docker credential injection
  • copyGitConfig: false — prevents host git config (with credential helpers) leaking in

Trade-offs

VariableWhen ClearedWhat Breaks
SSH_AUTH_SOCKSSH tools can't find agentCan't use host SSH keys (intended)
GPG_AGENT_INFOGPG can't find agentCan't sign with host GPG keys (intended)
BROWSERxdg-open/open failLinks won't open in host browser
VSCODE_IPC_HOOK_CLIcode command failsCan't open files in VS Code from terminal
GIT_ASKPASS / VSCODE_GIT_ASKPASS_*HTTPS credential helper disabledNo HTTPS git auth
VSCODE_GIT_IPC_HANDLEGit extension IPC disabledVS Code Git panel may lose some features
REMOTE_CONTAINERS_*Extension IPC disabledMinor feature loss

Remaining Risk

There is a window after container start (up to ~5 minutes while the 10 cleanup passes complete) where IPC sockets may temporarily exist. Each pass at 30s intervals removes any sockets that exist at that point, progressively closing the attack surface. During this window, a targeted attack could discover vscode-ipc-*.sock sockets via find /tmp and connect using VS Code's IPC protocol (HTTP POST over Unix socket with JSON payloads). After the final pass, the sockets are permanently removed. The env var clearing in Layer 2 ensures that standard tools and shell processes can never find these sockets regardless.

Verification

Run the verification script to confirm hardening is effective:

bash .claude/skills/devcontainer-security/scripts/verify-hardening.sh

See VERIFICATION.md for details.

DOCKER-PROXY.md

NODE-SETUP.md

SECURITY-HARDENING.md

SKILL.md

tile.json

VERIFICATION.md

WORKTREE-SUPPORT.md