← Default view
Atlas · expedition · drawing no. CC-LNX-2026 · sheet 00 of 07 · rev A

Containment stack for an
agentic coding CLI

Running Claude Code on a Linux daily driver, with a Proxmox mini PC available as an isolation boundary. Section, schedules and details for the seven angles of this expedition.
Scale not to scale Drawn 2026-07-30 Sheets 7 Refs 240
N General notes — read before building anything
  1. Build two setups, not one. One hardened daily driver, one disposable guest. Every attempt to make a single configuration serve both ends up either unusable or uncontained.
  2. Nothing enforced inside the agent's process is a boundary. Anthropic's docs say it outright — permission rules are enforced by Claude Code, not by the model [1] — and the Bash parser is "a permission gate, not a sandbox" [2].
  3. Rank containment by what actually caused losses: stop writes outside the workspace, then stop arbitrary egress, then stop reads of ~/.ssh and ~/.aws — in that order.
  4. Two controls survive every permission mode, including bypassPermissions: a permissions.deny rule and a PreToolUse hook returning deny [15][7]. Configure both — as blast-radius reduction, not as the boundary.
K Key plan — nested enclosures, plan view
L5 · KVM VM ON PROXMOX — HYPERVISOR BOUNDARY L4 · ROOTLESS PODMAN + PROXY-ONLY INTERNAL NET L3 · WHOLE-PROCESS bwrap WRAPPER L2 · BUILT-IN BASH SANDBOX (BASH SUBPROCESSES ONLY) L1 HOOK + DENY RULES · L0 PERMISSION POLICY — ADVISORY AGENT PROCESS SUBSTRATE · SHARED KERNEL — CONTAINED BY NOTHING ABOVE IT CVE-2026-23111 STOPS AT L5 OUTWARD
Fig. K — each enclosure is enforced further from the agent's process. Dashed amber = advisory. Solid = kernel-enforced. Double = hypervisor. Hatched = out of scope for every layer drawn.
A Section A–A · containment stack — six layers, top = innermost
L0POLICY
Permission rules in settings.json
Enforced by — Claude Code's own process
Not a boundary, and the docs never claimed it was. A denied curl was downgraded to a prompt by padding the command past a 50-subcommand parser cap [3][45]; deny rules went unenforced through symlinks (CVE-2026-25725) [4] and through the @../ attachment syntax [47]. Still worth writing — it stops the honest mistakes, which are most of them.
Boundary
Advisory
Daily friction
Configure anyway
L1GATE
permissions.deny + PreToolUse hook
Enforced by — Claude Code, but before any mode check
The only in-process controls that fire even under --dangerously-skip-permissions [15][7]. Use the hook to deny what the built-in parser misses: sudo, loader indirection via ld-linux, /proc/self/root path identity, environment runners like devbox run, and any attempt to set dangerouslyDisableSandbox. Still the same process — a compromised hook file is a compromised gate, so deny writes to it.
Boundary
Advisory / unconditional
Daily friction
Holds in every mode
L2KERNEL
Built-in Bash sandbox — bubblewrap ⭐ 8.2k + seccomp
Enforced by — the Linux kernel · covers Bash subprocesses only
The highest containment per minute of setup on Linux. Set sandbox.enabled: true and allowUnsandboxedCommands: false [8]. Two things to know: it restricts writes and network but permits reading the whole filesystem with no credential deny list by default, and it does not cover Read/Edit, hooks or MCP servers [14]. It has been escaped via symlink once already (CVE-2026-39861, CVSS 7.7) [5].
Boundary
Kernel · partial
Daily friction
Daily driver — on
L3KERNEL
Whole-process bwrap wrapper
Enforced by — the Linux kernel · covers the entire claude process tree
A ~30-line claude-jail script closes L2's gap: hooks, MCP servers and the built-in Read/Edit tools land inside the same filesystem view. Or let Anthropic maintain the wrapper — npx @anthropic-ai/sandbox-runtime ⭐ 4.8k. The price of admission is an unprivileged user namespace, which widens reachable kernel surface — see the substrate note below.
Boundary
Kernel · whole process
Daily friction
Daily driver — on
L4NS
Rootless Podman + proxy-only --internal network
Enforced by — namespaces + a proxy sidecar · shared host kernel
Use --userns=keep-id, the repo as the only bind mount, and egress through a proxy sidecar on an internal network. Not the NET_ADMIN-plus-iptables pattern of Anthropic's reference devcontainer, which allows unrestricted outbound DNS, allowlists all of GitHub's CIDR space, and hands the agent's own container CAP_NET_ADMIN [13][27]. Rootless Docker is a dead end: it maps your UID to container root, and the CLI refuses --dangerously-skip-permissions as root [28].
Boundary
Kernel · shared
Daily friction
Optional middle step
L5HYPER
KVM VM on the Proxmox mini PC — never LXC
Enforced by — KVM + a host-side guest-NIC firewall
The strongest boundary available, and the only one that also contains the build. Proxmox's own container docs say containers use the host kernel and "full virtual machines provide better isolation" [18]; LXC upstream says privileged containers "aren't and cannot be root-safe" [19]; Anthropic sends untrusted repositories to a dedicated VM for kernel-level separation [14]. Clone the repo inside the guest — no home mount. Drive it over SSH + tmux, Mosh if the laptop suspends. Disk-only snapshots roll back in seconds [21], and the guest firewall runs host-side as iptables so it cannot be switched off from inside [20].
Boundary
Hypervisor
Daily friction
Untrusted + unattended
SSUBSTRATE
The shared kernel — contained by nothing above it
Enforced by — nobody
L2 through L4 all buy their containment with unprivileged user namespaces, which widen the reachable kernel surface. CVE-2026-23111 is a concrete unprivileged-to-root path through exactly that [12][54]. L5 does not remove this — it relocates the trust boundary to KVM.
Boundary
None
Open — see stamp
B Assembly schedule — which bands go in which build

Assembly A · daily driver

Your own repos · interactive · every day
L0Permission rules — deny reads of credential paths
L1PreToolUse hard-deny hook
L2Built-in sandbox, failIfUnavailable: true
L3claude-jail wrapper or sandbox-runtime
L4Container layer — unnecessary here
L5VM — too much friction for daily work
Credentials: a FIDO2-backed SSH key [39] and one repo-scoped fine-grained PAT [40] — nothing else reachable.

Assembly B · untrusted & unattended

Someone else's repo · --dangerously-skip-permissions · overnight runs
L5KVM guest on Proxmox, cloned from a template
L5Guest-NIC firewall: LAN and host rejected outbound
L5Repo cloned inside · deploy key, read-only
L5Disk-only snapshot before each run
L0Deny rules still on — cheap, and they hold
LXC container — rejected, see below
Rebuild rather than repair: keep one base snapshot taken right after provisioning and roll back to it, never to yesterday's mess.
X Not to spec — evaluated and rejected
OptionWhy it fails hereRef
LXC container on Proxmox Shares the host kernel; Proxmox's own docs prefer full VMs for isolation, and LXC upstream states privileged containers cannot be root-safe. [18] [19]
firejail ⭐ 7.6k setuid-root by design, with a matching CVE history. You do not add a setuid-root binary to reduce the blast radius of a coding agent. [86] [87]
systemd-run --user Its strongest hardening directives are documented as applying to system services only, so the user-scope version buys much less than it appears to. [25]
Flatpak Wrong shape for a dev CLI, and roughly 42% of published apps neuter their own sandbox with broad filesystem permissions. [26] [85]
Rootless Docker Maps your UID to container root; Claude Code then refuses --dangerously-skip-permissions as root, so the one thing you wanted the container for stops working. [28] [73]
Anthropic's reference devcontainer, as shipped Good shape, weak boundary: unrestricted outbound DNS, all of GitHub's CIDR space allowlisted, CAP_NET_ADMIN granted to the agent container. Keep the shape, replace the firewall with a proxy sidecar. [13] [74]
Hand-rolled Landlock The right primitive to bet on long-term, wrong thing to write yourself today — let bubblewrap and the tools built on it carry it. [41] [76]
C Chart C · containment vs. daily friction — plotted from the seven angles
DAILY FRICTION → CONTAINMENT STRENGTH → THE KNEE L0 deny rules L1 PreToolUse hook L2 built-in sandbox L3 bwrap wrapper L4 rootless Podman L5 KVM VM systemd-run --user firejail devcontainer as shipped rootless Docker LXC Flatpak
Fig. C — L2 + L3 is the efficiency knee: near-VM filesystem containment at near-zero daily cost. Everything crossed out costs friction without buying a boundary. L5 is the only point above the knee, and it is bought with real friction.
Keynotes — three findings every angle hit independently
▲ 1Reads are wide open by default

The built-in sandbox restricts writes and network but permits reading the whole filesystem, with no credential deny list [8]. So ~/.ssh and ~/.aws are in scope unless you list them — and 2026's npm stealers name ~/.claude/.credentials.json explicitly [9]. On Linux the keyring is not a mitigation: once the login collection is unlocked, any process running as you reads every secret over D-Bus [10][38]. Encrypting at rest changes nothing; changing what the credential is does.

▲ 2Ubuntu 24.04+ silently disables the sandbox

Default AppArmor forbids bubblewrap from creating user namespaces, and a sandbox that fails to start degrades to unsandboxed execution with a warning [8][42]. Set sandbox.failIfUnavailable: true and install a bwrap AppArmor profile, or the whole local story is theatre — a specific trap for anyone arriving from WSL2, where none of this applied.

▲ 3The dependency is a likelier attacker than the model

preinstall scripts run before any agent permission check, and observed malware re-invokes an installed agent CLI with --dangerously-skip-permissions as a credential-hunting tool [11][69]. The build needs the same boundary as the agent — the strongest single argument for L5.

D Details — installed configuration, as drawn
1Daily driver — settings.json~/.claude/
// L0 + L1 + L2 in one file
{
  "permissions": {
    "defaultMode": "default",
    "disableBypassPermissionsMode": "disable",
    "deny": [
      "Read(//**/.env)", "Read(//**/.env.*)",
      "Read(~/.ssh/**)", "Read(~/.aws/**)",
      "Read(~/.gnupg/**)", "Read(~/.kube/**)",
      "Read(~/.config/gh/**)",
      "Read(~/.claude/.credentials.json)",
      "Edit(~/.claude/hooks/**)",
      "Edit(//**/.git/hooks/**)",
      "Bash(sudo *)", "Bash(git reset --hard*)",
      "Bash(git push --force*)"
    ]
  },
  "sandbox": {
    "enabled": true,
    "failIfUnavailable": true,   // keynote 2
    "allowUnsandboxedCommands": false,
    "autoAllowBashIfSandboxed": true,
    "network": { "strictAllowlist": true,
      "allowedDomains": ["api.anthropic.com",
        "registry.npmjs.org", "*.github.com"] },
    "filesystem": { "denyRead":
      ["~/.ssh", "~/.aws", "~/.gnupg", "~/.config/gh"] }
  }
}
2L3 — claude-jail, essential flags~/.local/bin/
#!/usr/bin/env bash
# refuse to run if the kernel won't give us a userns
bwrap --unshare-user --dev /dev true 2>/dev/null || exit 1

exec bwrap \
  --unshare-all --share-net --die-with-parent \
  --hostname claude-jail \
  --clearenv --setenv HOME "$HOME" \
    --setenv PATH /usr/local/bin:/usr/bin:/bin \
    --setenv TERM "${TERM:-xterm-256color}" \
  --proc /proc --dev /dev --tmpfs /tmp --tmpfs /run \
  --ro-bind /usr /usr --symlink usr/bin /bin \
  --ro-bind /etc/ssl /etc/ssl \
  --ro-bind "$(readlink -f /etc/resolv.conf)" /etc/resolv.conf \
  --bind "$HOME/.claude" "$HOME/.claude" \
  --bind "$PROJECT" "$PROJECT" --chdir "$PROJECT" \
  --cap-drop ALL \
  claude "$@"

# Ubuntu 24.04+: without this profile, bwrap cannot
# create a user namespace at all.
sudo tee /etc/apparmor.d/bwrap <<'EOF'
abi <abi/4.0>,
include <tunables/global>
profile bwrap /usr/bin/bwrap flags=(unconfined) {
  userns,
}
EOF
sudo systemctl reload apparmor
3L5 — guest-NIC firewall/etc/pve/firewall/210.fw
# enforced host-side as iptables — the guest
# cannot switch this off from inside
[OPTIONS]
enable: 1
policy_in: DROP
policy_out: DROP

[RULES]
# in: only your workstation, only ssh (+ mosh)
IN  ACCEPT -p tcp -dport 22 -source 192.168.1.50
IN  ACCEPT -p udp -dport 60000:61000 -source 192.168.1.50

# out: the internet, never the LAN or the host
OUT ACCEPT -p udp -dport 53 -dest 192.168.1.1
OUT REJECT -dest 192.168.1.0/24
OUT REJECT -dest 10.0.0.0/8
OUT REJECT -dest 172.16.0.0/12
OUT ACCEPT -p tcp -dport 443
OUT ACCEPT -p tcp -dport 22 -dest +forges
4L5 — snapshot & rollback drillProxmox host
# clean baseline, right after provisioning.
# never delete this one.
qm snapshot 210 base --description "clean toolchain"

# pre-flight before an unattended run
qm snapshot 210 "pre-$(date +%Y%m%d-%H%M)"

# undo — seconds, on ZFS or LVM-thin
qm rollback 210 base --start

# disk-only: omit --vmstate. RAM snapshots cost
# minutes and buy nothing you want here.
qm listsnapshot 210
qm delsnapshot 210 pre-20260730-0900

# the guest's only credential: a read-only
# deploy key, scoped to one repository
ssh-keygen -t ed25519 -f ~/.ssh/deploy_myrepo -N ""
git clone git@github.com:me/myrepo.git ~/src/myrepo
R Schedule of known breaches — why L0 and L1 are drawn dashed
RefMechanismEffectLayerSource
CVE-2026-25725Deny rules unenforced through symlinksDenied path readableL0[4]
CVE-2026-39861Sandbox escaped via symlink · CVSS 7.7Write outside the sandboxL2[5]
Parser capCommand padded past a 50-subcommand limitDenied curl downgraded to a promptL0[3] [45]
Loader indirectionAgent reasoned its way into disabling its own sandbox, then loaded a blocked binary through ld-linux.soexecve gate sidestepped entirelyL0 · L2[6] [44]
@../ syntaxAttachment path escaped a Read deny ruleOut-of-tree file readL0[47]
Env prefixDeny bypassed with an environment-variable command prefixDenied command ranL0[48]
SubagentsTask-tool subagents ran 22+ bash commands including credential readsGate bypassed by delegationL0 · L1[46]
SOCKS5 null byteAllowlist bypass, ~130 affected releases — patched with no CVE and no changelog entryNetwork allowlist defeatedL2[43]
DNS exfilData smuggled out over DNS requestsEgress with no HTTPL2 · L4[32]
Devcontainer DNSReference firewall allows outbound UDP/53 to any resolverDNS tunnelling out of the containerL4[13]
CVE-2026-23111nftables unprivileged-to-root, reachable through the user namespace bubblewrap needsRoot on the hostSubstrate[12]
Realized lossAgent ran git reset --hard / git checkout unpromptedUncommitted work permanently destroyedAll[30] [31] [80]
Realized lossnpm supply-chain malware re-invokes an installed agent CLI with --dangerously-skip-permissionsCredentials swept from $HOMEAll[11] [9]
Sourcing caveat — the Claude Code CVE details come from vulnerability-database aggregators and security-vendor blogs, not from Anthropic advisories. Treat version-specific claims as directional.
S Ancillary specifications — identity and undo

Spec S1 · the agent's identity

Sheet 06 — credential isolation · survey
Assume anything readable under $HOME is already exfiltrated. Do not try to hide the credential — change what it is.

· Git access: a FIDO2-backed SSH key — the private half cannot leave the token.
· Forge API: one fine-grained PAT, scoped to one repo [40]. In the VM, a read-only deploy key instead.
· Everything else: short-lived and injected at process start, never written to a dotfile [78].
· Not a mitigation: the GNOME keyring. Unlocked login collection = every secret readable over D-Bus by any process running as you [10].
· Keep a revocation drill, written down, rehearsed once.

Spec S2 · undo and evidence

Sheet 07 — audit trail and recovery drills · recon
The built-ins are convenience layers, not evidence.

· Real undo: git commits, plus reflog for the rest. Commit before handing the agent the keyboard.
· Real audit log: a PostToolUse hook appending outside the repo [64] — because the agent can write to anything inside it.
· Transcripts expire after 30 days [34], and /rewind cannot undo what a bash command did, nor a subagent's edits [35].
· Rehearse one restore. "A backup you have never restored from is not a backup, it is a theory" [36].
I Sheet index — the seven angles of this expedition
SheetTitle & findingDepthRefsReadLayers
01 Threat model and blast radius The realized 2026 losses come from the agent's own destructive git commands and from supply-chain malware that weaponizes an installed agent CLI — not from the exotic prompt-injection scenarios. Containment must stop out-of-workspace writes, egress, and credential reads, in that order. survey2810 minframes all
02 Claude Code's native guardrails in 2026 Permission rules are an in-process policy layer, not a security boundary; only the bubblewrap-backed Bash sandbox has OS teeth, and it covers Bash alone — with the exact syntax, the documented holes, the CVEs, and the two controls that hold in every mode. expedition4026 minL0 · L1 · L2
03 Linux-native sandboxing on the workstation Turn on the built-in bubblewrap sandbox, then wrap the whole process in a 30-line bwrap script. systemd-run --user, firejail and Flatpak are the wrong tools for this job, and Landlock is the primitive to bet on but not to hand-roll. expedition5225 minL2 · L3
04 Containers as the containment layer Rootless Podman plus a proxy-only internal network beats Anthropic's reference devcontainer; the reference firewall leaks DNS and hands the agent NET_ADMIN. survey3412 minL4
05 Proxmox as the isolation boundary — KVM, not LXC Proxmox's own docs say containers share the host kernel and LXC upstream says privileged containers cannot be root-safe — plus the wiring, firewall rules, snapshot policy and sizing that make a dedicated agent guest actually usable. expedition4224 minL5
06 Credential isolation for an agentic coding CLI An agent that can read $HOME holds your whole identity, and on Linux the keyring does not help — so give the agent its own short-lived, narrowly scoped credentials instead. survey3211 minspec S1
07 Audit trail and recovery drills Git commits are your real undo and a PostToolUse hook is your real audit log — Claude Code's own transcripts and /rewind are convenience layers that expire, miss bash, and are writable by the agent. recon123 minspec S2
Rev. pending · open question · none of the seven angles closes this

Nothing drawn here contains a kernel exploit, and L5 only moves the trust boundary to KVM. If your threat model includes a targeted attacker rather than opportunistic supply-chain malware, the honest answer is that the agent host should be disposable — which argues for automating VM re-provisioning rather than snapshotting one long-lived guest.

Project
Claude Code on Linux
Drawing
CC-LNX-2026 · Section A–A
Sheet
00 of 07 · index
Depth
expedition
Angles
7 · all successful
References
240
Scale
Not to scale
Date
2026-07-30
Rev
A — first issue
Status
Issued for construction
Substrate risk
Unresolved
Proxmox logo
Plate 1 · Proxmox VE — the L5 host
Abstract glyph grid from Anthropic's sandboxing engineering post
Plate 2 · Anthropic on sandboxing — the L2 design
Line illustration of four rack-optimised servers stacked
Plate 3 · rack-optimised guests — the stack, in metal
Tux, the Linux mascot
Plate 4 · the shared kernel — the substrate nothing above contains