Skip to content

evsor/devcontainers

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

devcontainers

Local Docker images for VS Code Dev Containers.

Images

Image Local tag Contents
Base devcontainers/base Debian slim, non-root user, common tools
Go devcontainers/go Base + Go runtime + gopls
Python devcontainers/python Base + Python 3 + uv

Building

Requires Docker and make.

make all      # build all images in dependency order
make base     # base image only
make go       # go image (base built first)
make python   # python image (base built first)
make clean    # remove all local images

Using a devcontainer

Each language directory contains a .devcontainer/devcontainer.json ready to copy into a consuming repo:

cp -r go/.devcontainer /path/to/your-project/

Open the project in VS Code and select Reopen in Container. The image must be built locally first (make go or make all).

Modifying devcontainer settings

The devcontainer.json files are generated — do not edit them directly. Edit the source templates instead:

File Purpose
templates/base.json Shared mounts, extensions, and postCreateCommand
templates/go.json Go image name and Go-specific extensions
templates/python.json Python image name and Python-specific extensions

After editing, regenerate and commit:

make devcontainers
git add go/.devcontainer/devcontainer.json python/.devcontainer/devcontainer.json

Adding a new language

  1. Create <lang>/Dockerfile extending devcontainers/base
  2. Add a make <lang> target in Makefile
  3. Create templates/<lang>.json with name, image, and any language extensions
  4. Run make devcontainers to generate <lang>/.devcontainer/devcontainer.json

Claude Code Sandbox

The root .devcontainer/devcontainer.json configures a hardened container for running Claude Code autonomously inside this repository. The goal is defense-in-depth: limit the blast radius of a compromised or misbehaving agent while keeping legitimate development tasks (git over HTTPS, web access to allowed hosts, full filesystem access within the workspace) fully functional.

What is hardened and how

Firewall (base/init-firewall.sh)

Applied at container start via postStartCommand running as root (via sudo).

  • Immediate lockdown: DROP policy is set on all chains at the very start of the script, before any rule is flushed. This eliminates the window where outbound traffic would be unrestricted during setup.
  • IPv6: disabled at the kernel level via --sysctl in runArgs and additionally blocked via ip6tables rules.
  • Outbound allowlist: only TCP port 443 to a fixed set of IPs is permitted. Everything else is explicitly REJECTED (not dropped) for fast failure feedback.
  • Allowlist contents: Anthropic API CIDR (160.79.104.0/23), VS Code marketplace and update hosts, GitHub IPs. GitHub IPs are fetched from the GitHub API and baked into the image at build time — no runtime dependency on GitHub for the allowlist itself.
  • Port 22 blocked: SSH outbound is not permitted. Git must use HTTPS with a personal access token.

Linux capabilities and security options (devcontainer.json runArgs)

  • --cap-drop=ALL removes every Linux capability from the bounding set.
  • Re-added selectively: NET_ADMIN and NET_RAW (needed by iptables/ipset), SETUID and SETGID (needed by sudo — see trade-offs).
  • --security-opt=apparmor:docker-default explicitly enforces Docker's default AppArmor profile.

Process and filesystem limits (runArgs)

  • --pids-limit=256 caps the total process count, preventing fork bombs.

VS Code IPC hardening

VS Code injects environment variables into the container at attach time that act as bridges back to the host:

Variable What it enables
VSCODE_IPC_HOOK_CLI Execute arbitrary commands on the host
VSCODE_GIT_IPC_HANDLE Access host git credentials via VS Code's IPC bridge
GIT_ASKPASS Trigger credential prompts on the host
REMOTE_CONTAINERS_IPC General extension IPC bridge to the host
BROWSER Open a browser on the host side

VS Code re-injects these at multiple startup phases — some sockets appear 60+ seconds after attachment — so a single point of clearing is not sufficient. Three layers are applied:

  1. remoteEnv sets them all to empty strings at attach time.
  2. base/harden-shell.sh unsets them again at shell startup. It is sourced before the interactive guard in ~/.bashrc (so it runs even in non-interactive contexts where .bashrc is sourced) and is also set as BASH_ENV so it is sourced by non-interactive bash scripts (the kind agents typically use).
  3. base/ipc-socket-cleanup.sh runs as a background daemon for 5 minutes after container start, deleting the socket files themselves at 30-second intervals.

Credential isolation (remoteEnv, base/harden-shell.sh, base/Dockerfile)

  • SSH_AUTH_SOCK is cleared — the host SSH agent is not forwarded into the container.
  • GIT_SSH_COMMAND=false causes any SSH-based git operation to fail immediately.
  • GIT_CONFIG_COUNT=1 / GIT_CONFIG_KEY_0=credential.helper / GIT_CONFIG_VALUE_0= injects a credential config override via git's environment variable mechanism, which has the highest possible priority — higher than any file-based config. VS Code overwrites ~/.gitconfig with its own credential helper at attach time; this env var override wins regardless.
  • A baseline ~/.gitconfig with credential.helper= is baked into the image as defense-in-depth.

VS Code settings (customizations.vscode.settings)

  • task.autoDetect: off — VS Code will not automatically detect and offer to run workspace tasks. Workspace tasks are a documented code execution vector (malicious repos can include tasks that run on open).
  • telemetry.telemetryLevel: off — prevents VS Code from sending usage and diagnostic data.

Trade-offs

SETUID/SETGID capabilities are available These are required for sudo's setuid mechanism to call setgid(0). The consequence is that any setuid binary in the image could potentially be exploited to escalate privileges. The risk is low — the image installs very few packages, none of which add additional setuid binaries beyond sudo itself — but it is non-zero.

No --security-opt no-new-privileges This flag would prevent setuid binaries from gaining privileges, eliminating the above risk entirely. It is incompatible with sudo: sudo is a setuid binary, and no-new-privileges prevents setuid binaries from gaining new privileges. Enabling it would require running the firewall from a root entrypoint before VS Code attaches (e.g., via Docker ENTRYPOINT) rather than via sudo in postStartCommand.

Container-start to firewall-active window The firewall is activated by postStartCommand, which runs a few seconds after the container starts and VS Code attaches. Outbound traffic is unrestricted during that window. The window inside the script itself is eliminated (DROP policy set before any flush), but the startup window is inherent to the postStartCommand approach.

GitHub IP allowlist goes stale between rebuilds GitHub IP ranges are baked in at image build time. New ranges GitHub adds between rebuilds will be blocked; retired ranges will remain in the allowlist. Rebuild the base image periodically to refresh the allowlist.

IPC socket cleanup is time-bounded The socket cleanup loop runs for 5 minutes. Env var clearing (layers 1 and 2) is permanent for the container lifetime, but VS Code IPC socket files created after the loop finishes persist on disk.

Sudo resets the firewall The agent can run sudo /usr/local/bin/init-firewall.sh at any time. The script is owned by root and not writable by the user, so the agent cannot modify the allowlist. However, the agent could choose when to trigger a reset, momentarily disrupting established connections.


Known bypass vectors

DNS tunneling Outbound UDP port 53 is permitted — it is required for hostname resolution. An agent can encode and exfiltrate data in DNS query payloads to a nameserver it controls. This is a standard technique for bypassing network-isolated environments and cannot be closed without breaking DNS entirely.

Allowed hosts as communication channels GitHub and Anthropic's API are both reachable. An agent can push content to GitHub repositories, create gists or issues, or use the Anthropic API itself to relay data or receive instructions from outside the container.

Anthropic CIDR breadth The Anthropic allowlist covers the CIDR block 160.79.104.0/23 (512 addresses), not a single IP. Any service reachable at an address in that range can be contacted, not only the Claude API endpoint.

Workspace secrets The agent has full read/write access to all files in the mounted workspace directory. Secrets stored there — .env files, config files with API keys, credentials — can be read and exfiltrated through the allowed network channels.

IPC channels after the 5-minute cleanup window After the socket deletion loop finishes, VS Code IPC socket files created by later VS Code processes persist. If VS Code re-injects the IPC environment variables into a new shell process that starts after the shell hardening has run, that shell would have them cleared — but a process spawned without going through a bash shell (e.g., via VS Code's task runner or extension APIs) may retain them.

git credential helper before shell hardening runs The GIT_CONFIG_COUNT override clears the credential helper for all processes that inherit the hardened environment. A git process invoked before bash starts — before harden-shell.sh has executed — would use whatever is in ~/.gitconfig, which VS Code overwrites with its own credential bridge.

Kernel escape A kernel container-escape vulnerability would bypass all userspace hardening. --cap-drop=ALL and AppArmor reduce the exposed syscall surface but cannot prevent a sufficiently sophisticated kernel exploit.

About

Local development containers

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors