Local Docker images for VS Code Dev Containers.
| 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 |
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 imagesEach 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).
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- Create
<lang>/Dockerfileextendingdevcontainers/base - Add a
make <lang>target inMakefile - Create
templates/<lang>.jsonwithname,image, and any language extensions - Run
make devcontainersto generate<lang>/.devcontainer/devcontainer.json
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.
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
--sysctlinrunArgsand 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.
--cap-drop=ALLremoves every Linux capability from the bounding set.- Re-added selectively:
NET_ADMINandNET_RAW(needed by iptables/ipset),SETUIDandSETGID(needed by sudo — see trade-offs). --security-opt=apparmor:docker-defaultexplicitly enforces Docker's default AppArmor profile.
--pids-limit=256caps the total process count, preventing fork bombs.
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:
remoteEnvsets them all to empty strings at attach time.base/harden-shell.shunsets them again at shell startup. It is sourced before the interactive guard in~/.bashrc(so it runs even in non-interactive contexts where.bashrcis sourced) and is also set asBASH_ENVso it is sourced by non-interactive bash scripts (the kind agents typically use).base/ipc-socket-cleanup.shruns as a background daemon for 5 minutes after container start, deleting the socket files themselves at 30-second intervals.
SSH_AUTH_SOCKis cleared — the host SSH agent is not forwarded into the container.GIT_SSH_COMMAND=falsecauses 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~/.gitconfigwith its own credential helper at attach time; this env var override wins regardless.- A baseline
~/.gitconfigwithcredential.helper=is baked into the image as defense-in-depth.
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.
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.
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.