Module 3 — Control

Execution Environments and Sandboxing

Reading time ~16 min · Last reviewed Sep 2026 · Quiz at the end

Lesson 11 was about what an agent can do: the shape of its tools, their contracts, their error messages. This lesson is about where those actions actually happen. The moment an agent can run a shell command, execute a script it just wrote, or install a package, a question appears that no tool schema answers: what machine is this running on, what can the process touch, what can it reach over the network, and what is left behind when it finishes? For a chat assistant that question never comes up. For a coding agent, a data agent, or anything using "code mode", it is the difference between a contained mistake and an incident.

Most teams answer it by accident. The agent runs as the developer, on the developer's laptop, with the developer's SSH keys, cloud credentials, and browser cookies all in reach, and the only control is a permission prompt the developer has learned to click through. That arrangement works until the model misreads an instruction, a dependency ships a malicious postinstall script, or a web page smuggles in a command. This lesson treats the execution environment as what it is: a first-class harness subsystem with its own design space, trade-offs, and failure modes.

Why the execution environment is its own subsystem

The seven subsystems in lesson 2 include tools and guardrails, and it is tempting to file the sandbox under one of them. That misses what makes it distinct. A tool definition says what an action is. A permission policy says whether the agent may take it. The execution environment decides what the action can affect once it runs, and that is the only one of the three that still holds when the other two are wrong. A shell tool is a universal tool: once the model can run bash, it can do anything the process can do, and no amount of careful tool description narrows that. The only thing that narrows it is the boundary around the process.

There is also an operational argument. Asking a human to approve every command does not scale, and people stop reading prompts they see all day. Anthropic reported that adding OS-level filesystem and network isolation to Claude Code let it drop permission prompts substantially, reporting that sandboxing cut permission prompts by 84% (Anthropic, 2025). The approval experience itself is lesson 13's subject. The point here is structural: a good sandbox is what lets you ask fewer questions, because the answer to "what's the worst this command can do?" becomes "not much".

Key idea

Permissions decide whether an action runs. The execution environment decides what a running action can reach. Design the environment so that the worst case of an approved-but-wrong action is cheap, and you can afford to approve far more automatically. Prompts are a fallback for what the boundary can't express, not the boundary itself.

The isolation spectrum

"Sandbox" covers a wide range of mechanisms, from a flag on a subprocess call to a fresh virtual machine in someone else's data center. They sit on a spectrum that trades startup cost and fidelity against the strength of the boundary. Nothing on the spectrum is universally correct. The right point depends on who wrote the code being run, what the agent needs to reach, and how many environments you need per hour.

LevelExamplesBoundaryStartupFidelity to the user's machineMain risk
Host process + promptsAgent runs as the user, asks before commandsNone beyond the user's own permissionsZeroPerfect: it is the user's machineEverything the user can touch is in scope; approval fatigue
OS-level sandboxLinux bubblewrap and Landlock, macOS SeatbeltKernel-enforced file (and sometimes network) restrictions on one process treeMillisecondsHigh: same kernel, same toolchain, same filesShared kernel; policy gaps; partial enforcement on older kernels
ContainerDocker/OCI, optionally with gVisorNamespaces and cgroups; still a shared host kernel unless a user-space kernel is addedSub-second to secondsMedium: you choose the image, so it may not match localKernel escape; misconfigured mounts and privileges
MicroVMFirecrackerHardware virtualization; separate guest kernelFractions of a secondMedium: a real Linux, but a clean oneHypervisor bugs (rare); image and snapshot management cost
Remote / ephemeral VM or managed sandboxHosted agent sandboxes, cloud dev environmentsSeparate machine, often separate networkSeconds, unless pre-warmed or lazily provisionedLow to medium: the user's local state isn't thereCost; latency; data residency; syncing work back

A few of these deserve a concrete anchor. Landlock is a stackable Linux security module whose stated goal is "restriction of ambient rights" for a set of processes, and it lets an unprivileged process sandbox itself; newer ABI versions add TCP and UDP restrictions on top of filesystem rules. gVisor is "an application kernel" that runs in user space and plugs into Docker and Kubernetes as the runsc runtime, which moves a container's syscalls off the host kernel. Firecracker advertises boot times under 125 ms, under 5 MiB of overhead per microVM, and up to 150 microVMs created per second per host, and it is the technology under AWS Lambda. Those numbers are why microVMs stopped being exotic for agent workloads: a per-task VM now costs roughly what a per-task container used to.

Real harnesses use several levels at once. Claude Code's local sandbox is built on bubblewrap on Linux and Seatbelt on macOS, released as an open-source sandbox-runtime (Anthropic, 2025). Codex on Linux uses bubblewrap as its default filesystem sandbox, with Landlock as a legacy path (openai/codex, 2026). DeepSeek Harness is the clearest example of a harness that is explicit about where one level stops. Its sandbox package group states that confinement is "same-world only": it shares the host kernel and filesystem, while containers, microVMs, and remote executors replace whole capabilities rather than plugging into the sandbox layer (DeepSeek Harness, 2026). That is the right mental model. An OS sandbox narrows what a process on this machine can do. Moving to a container or VM changes which machine the process is on, and that belongs to a different part of the design, which we come to below.

Pitfall

Treating "it runs in Docker" as a security claim. A default container shares the host kernel, often runs as root inside, and is frequently started with the project directory, the Docker socket, or ~/.aws mounted "for convenience". Any one of those turns the container into a thin costume over the host. If the code being run is untrusted (and model-generated code that reads the internet should be treated as untrusted), either add a real kernel boundary (gVisor, a microVM) or be precise about what the container does and does not protect.

What a good OS-level sandbox looks like

DeepSeek Harness's sandbox package is worth reading closely because its contracts spell out properties that many harnesses leave implicit (DeepSeek Harness sandbox subsystem, 2026). Four stand out.

There is a fifth detail that matters more than it looks. The wrapped result carries two separate classifiers for stderr: one that recognizes the command being blocked while the sandbox works correctly, and one that recognizes the sandbox runner itself failing. The harness checks the second first and reports infrastructure failure, never an ordinary task failure. Without that split, a broken sandbox looks to the model like a flaky test, and the agent will spend twenty turns "fixing" code that was never the problem.

Filesystem and network policy

Whatever level you choose, the policy inside it comes down to two questions: which paths can the process read and write, and which hosts can it talk to. Anthropic's write-up gives the reason you need both in one sentence: without network isolation a compromised agent can exfiltrate things like SSH keys, and without filesystem isolation it can escape the sandbox and gain network access (Anthropic, 2025). Either boundary alone is a door with the lock on the wrong side.

The filesystem defaults that hold up in practice are simple. The workspace is writable, and nothing else is apart from a private temp directory. System directories and toolchains are mounted read-only so the agent can use compilers and interpreters but not modify them. Secret-bearing paths (~/.ssh, cloud credential files, .env files, the git credential helper's store) are not merely read-only but absent. Version-control metadata deserves a thought of its own: an agent that can rewrite .git/hooks can plant code that runs later, outside the sandbox, the next time a human commits.

Network policy is where teams are most tempted to punt, because agents genuinely need the network: package registries, documentation, the API under test. The workable pattern is default-deny egress through a proxy that enforces a domain allowlist, which is how Claude Code's sandbox does it: a proxy "enforces restrictions on the domains that a process can connect to" and asks the user about new domains. Note what DeepSeek Harness's modes explicitly do not cover: its sandbox modes govern filesystem effects only, and the docs state that network and process visibility fall outside them. That is honest documentation, and it is a warning. If your sandbox layer only handles files, network control has to live somewhere else, and you should know where.

Here is what a combined policy might look like. It is illustrative, not the schema of any real product, but every field corresponds to a decision you will have to make somewhere.

# Illustrative sandbox policy (not a real product's schema)
sandbox:
  isolation: microvm            # host | os | container | microvm | remote
  image: agent-base:2026-09     # pinned; match what the model is used to
  fresh_per_task: true          # no state carried between tasks
  snapshot_on_exit: true        # keep for debugging and replay

filesystem:
  workspace:
    path: /workspace
    mode: read-write
  read_only:
    - /usr
    - /opt/toolchains
  deny:                         # not mounted at all
    - ~/.ssh
    - ~/.aws
    - ~/.config/gcloud
    - "**/.env"
  protect:                      # writable workspace, but these stay read-only
    - /workspace/.git/hooks
    - /workspace/.git/config

network:
  default: deny
  egress_proxy: true
  allow:
    - registry.npmjs.org
    - pypi.org
    - files.pythonhosted.org
    - api.internal.example.com:443
  on_unknown_domain: ask        # ask | deny

credentials:
  in_sandbox: none              # tokens never enter the sandbox
  git: proxy                    # a proxy outside the boundary attaches scoped auth
  tools: vault_proxy            # tool calls get credentials outside the sandbox

resources:
  cpu: { guaranteed: 2, limit: 4 }
  memory_gib: { guaranteed: 4, limit: 12 }
  disk_gib: 20
  wall_clock_min: 45
  max_processes: 512

on_violation:
  report_as: sandbox_denial     # distinct from task failure in the transcript
  escalation: one_time_approval

Two notes on that allowlist. First, every domain on it is a potential exfiltration channel: an agent that can reach a code-hosting site can usually write to it, and a package registry that accepts downloads may also accept publishes. The finer points of that threat, including why private data plus untrusted input plus an outbound channel is the dangerous combination, are lesson 14's territory. Second, the allowlist is also a reproducibility decision. We'll return to that.

Brain versus hands: keep credentials out of the box

The most important architectural idea in this lesson comes from Anthropic's write-up on Managed Agents (Anthropic, 2026). Their earlier design put the harness, the session, and the execution environment in one container. That made each container a "pet": if it died, the session died with it, and every environment had to be provisioned before the model could say anything. The redesign separates the brain (the model plus the harness loop) from the hands (sandboxes and other execution environments) and from the session log. The harness no longer runs inside the sandbox. It calls the sandbox the way it calls any other tool, through an interface shaped like execute(name, input) → string, and doesn't care whether the thing on the other side is a container, a phone, or an emulator.

Two consequences follow. The first is operational: sandboxes become cattle. A failed container is replaced from a standard recipe instead of nursed back to health, and because containers are provisioned only when the agent actually needs one, Anthropic reports median time-to-first-token fell by roughly 60% and p95 by more than 90%. The second is security, and it is the one to copy even if you never run a hosted agent: credentials never enter the sandbox where model-generated code runs. For git, a repository token is used during sandbox initialization to clone and wire up the remote, so push and pull work without the agent ever holding the token. For MCP tools, OAuth tokens live in a vault outside the sandbox, and tool calls go through a proxy that fetches the credential on the way out. Claude Code on the web applies the same pattern with a git proxy that checks the operation and attaches scoped credentials outside the boundary (Anthropic, 2025). Cloudflare's code-mode sandbox reaches the same place from a different direction: generated code cannot call fetch() at all, and reaches services only through bindings, so API keys are never visible to the code (Cloudflare, 2025).

In code, the separation is almost boring, which is the point:

# Sketch: the harness (brain) holds credentials; the sandbox (hands) never does.
class Harness:
    def __init__(self, sandboxes, vault, log):
        self.sandboxes = sandboxes   # provisioned lazily, replaceable
        self.vault = vault           # lives outside every sandbox
        self.log = log               # durable session log, outside the context window

    def run_tool(self, call):
        self.log.append(call)
        if call.name == "bash":
            box = self.sandboxes.get_or_create(call.session_id)
            result = box.execute("bash", call.input)      # no secrets passed in
        elif call.name.startswith("mcp."):
            token = self.vault.token_for(call.name)       # fetched outside the box
            result = mcp_proxy.call(call.name, call.input, token)
        self.log.append(result)
        return result
Key idea

Design the sandbox so it contains nothing worth stealing. Filesystem and network policy limit what a compromised process can do; keeping credentials outside the boundary limits what it can take. A sandbox holding a long-lived token is a vault with the combination taped to the door. Put the brain and the secrets on one side, the hands on the other, and let a proxy you control attach authority to specific, inspected requests.

The execution world as a seam

Once the execution environment is a separate thing, a design question appears that most harnesses get wrong at least once: which tools move when the environment moves? DeepSeek Harness answers it with a concept it calls the execution world. Its architecture doc states that filesystem and subprocess providers "share one execution world," so pointing them at a remote sandbox moves Bash, PTY, and LSP with them, with no provider forks (DeepSeek Harness architecture, 2026). Its capability-seams catalog shows the machinery: filesystem, subprocess, and sandbox seams each have local providers and SSH providers (fs-ssh, subprocess-ssh, sandbox-ssh), all hanging off one connection owner, (DeepSeek Harness capability seams, 2026), and even the programmatic tool-calling runtime takes its working directory in that same execution world (DeepSeek Harness PTC runtime, 2026).

The reason this matters is a failure mode you can call split-brain. If the file tools read and write on the laptop while bash runs in a remote container, the agent edits a file, runs the tests, and the tests run against a different copy. The model sees green tests for code it didn't write, or red tests for a fix it already made, and draws exactly the wrong conclusions. Treat "where do files live and where do processes run" as one decision, owned in one place, and make every tool that touches either one go through it. Lesson 23 takes DeepSeek Harness apart in full; for this lesson, the seam is the thing to steal.

Illustrative example

A team moves its coding agent from developers' laptops to per-task cloud VMs to get isolation. They reroute the shell tool to the VM but leave the file-edit tool writing locally, planning to sync "later". For a week, pass rates drop and transcripts show the agent re-applying the same edit, running tests, and concluding the test runner is broken. Nothing is wrong with the model or the tests. Two tools disagree about which machine the repository is on. The fix is not a better prompt; it is routing both tools through one environment abstraction.

Models are trained in sandboxes

There is a quieter reason to take the execution environment seriously: it is where frontier models learned to act. DeepSeek's V4 release describes DSec (DeepSeek Elastic Compute), a Rust platform exposing "four execution substrates behind one Python SDK": function calls, containers, Firecracker microVMs, and full QEMU VMs, with one cluster running hundreds of thousands of concurrent sandboxes (DeepSeek via Hugging Face, 2026). The post says V4's agent behavior was trained with RL against real tool environments, and highlights fast startup from layered storage, preemption-safe trajectory replay so interrupted training steps don't re-run tool calls, and a uniform API so training harnesses can target any substrate without rewriting.

What follows is inference, not something DeepSeek or anyone else has published: if a model spent its agentic training issuing commands inside Linux sandboxes with a particular shape (a standard userland, common toolchains, recognizable denial errors, a working directory it owns), then a harness environment that looks like that is likely to get better behavior than an exotic one. DeepSeek has not said V4 was trained inside DeepSeek Harness, and you should not assume any vendor's training environments match its shipped harness. But the direction of the argument holds for any model. Prefer boring, standard environments. Make sandbox denials show up as clear, conventional errors (EACCES, "permission denied", a named blocked domain) rather than hangs or silent truncation. Don't invent a bespoke shell dialect the model has never seen. The model's competence is partly a property of the environment it was trained in, and you're choosing how close to it you stay.

Clean state and reproducibility

A sandbox is also a reproducibility tool, and this is often where it pays for itself before any security incident. An agent working in a long-lived environment accumulates invisible state: packages it installed three tasks ago, a background server still bound to a port, a modified config file, a cache that masks a missing dependency. The next task inherits all of it. Results stop being a function of the task and start being a function of history.

The patterns that fix this are well established. Fresh environment per task, built from a pinned image, is the default for evaluation and for any hosted agent. Snapshot and restore lets you keep a warm base image (dependencies installed, repository cloned) so freshness doesn't cost a five-minute setup every time, and lets you freeze the end state of a failed run for debugging. Replay of the tool-call trajectory, which DSec builds in for training, is the same idea applied to the log rather than the disk; it is also why lesson 4 and lesson 15 care so much about durable session logs. On a developer machine, where full VMs are overkill, git worktrees give each parallel agent its own checkout of the repository, so two agents never edit the same working tree. A worktree isolates files, not processes or network, so pair it with an OS sandbox rather than treating it as one.

Network access is the reproducibility leak people forget. When Terminal-Bench 2.1 revised its predecessor, 9 of the 28 fixed tasks were broken by external dependencies, because "internet access introduces external dependencies that can change over time" (Terminal-Bench, 2026). The same thing happens to production agents: a task that passed last month fails today because a package released a new major version. Pinned images, lockfiles, and a registry mirror inside the allowlist make the environment's contents a decision rather than a coincidence.

Pitfall

Reusing one long-lived sandbox per user "for speed" without a reset policy. It feels efficient and behaves like shared mutable global state: failures become unreproducible, one task's leftovers change another's outcome, and anything a compromised run planted stays around. If you keep environments warm, snapshot a known-good state and restore from it, rather than letting the environment drift.

Resource limits change behavior

CPU, memory, disk, process count, and wall-clock limits look like operational settings. They are behavioral ones. Anthropic ran Terminal-Bench 2.0 with the same model, harness, and tasks across six resource configurations, from strict enforcement of each task's specified resources up to uncapped, and found a 6-point swing in success rate between the extremes (p < 0.01) (Anthropic, 2026). Infrastructure errors fell from 5.8% to 0.5%, but that wasn't the whole story. Above about 3x the specified resources, success rose faster than errors fell: generous limits let agents succeed with different strategies, such as installing a full data-science stack, while tight limits rewarded agents that wrote lean code quickly and punished ones that tried to install heavy dependencies first.

So the environment doesn't just gate success; it selects for a style of work. Two practical consequences. For production, resource limits belong in the harness spec, versioned and reviewed like a prompt, because changing them changes what the agent does. Anthropic's recommendation to specify both a guaranteed allocation and a separate hard ceiling, rather than one pinned number, applies directly (a 3x ceiling cut infra errors by about two-thirds while keeping the score lift modest). Make out-of-memory kills and timeouts show up in the transcript as what they are, not as generic command failures the model will try to debug. For evaluation, the same post advises skepticism toward leaderboard gaps under 3 points until configurations are documented and matched. Lesson 19 picks up that thread; the short version is that an agent score without its resource configuration is not fully specified.

Code mode makes sandboxes mandatory

The last reason execution environments are moving from optional to mandatory is a shift in how tools get called. In "code mode", or programmatic tool calling, the model writes a short program that calls tools as functions, filters and joins their results, and returns only what matters, instead of emitting one tool call per model turn. Cloudflare's framing is that LLMs "have seen a lot of code" but not many tool calls, so they're better at the former; its implementation turns MCP tools into a TypeScript API and runs each generated snippet in a fresh V8 isolate (Cloudflare, 2025). Anthropic's version presents MCP servers as code APIs on a filesystem and reports a Drive-to-Salesforce workflow falling from 150,000 tokens to 2,000 (Anthropic, 2025); its API-level programmatic tool calling cut average token use on complex research tasks by 37% (Anthropic, 2025). DeepSeek Harness ships the pattern as ptc-runtime: the model writes one program against host-provided async bindings, which runs in a fresh Node process under the configured sandbox policy, with no state carried from earlier programs (DeepSeek Harness, 2026).

Every one of these designs pairs code execution with a sandbox, and Anthropic says why plainly: running agent-generated code "requires a secure execution environment" with sandboxing, resource limits, and monitoring. Code mode turns every tool-using agent into a code-executing agent. Its benefits for context are large (lesson 5 explains why keeping intermediate results out of the window matters), but it means the question at the top of this lesson now applies to agents that never had a shell tool. Notice, too, the shape these runtimes converge on, which is the shape of this whole lesson: fresh environment per program, no ambient network, capabilities passed in as bindings, and credentials held by the host rather than the code.

Choosing a point on the spectrum

Pulling it together, a reasonable default ladder for most teams looks like this:

SituationSensible default
Developer running a coding agent locally on their own repoOS-level sandbox (workspace-write, default-deny network with allowlist), worktree per parallel agent, secrets paths not mounted
CI or batch agents on trusted codeFresh container per task from a pinned image; no credentials inside; scoped proxy for git
Agents running code influenced by untrusted input (web content, user uploads, third-party repos)MicroVM or gVisor per task; strict egress; snapshot on exit for forensics
Hosted multi-tenant agent productBrain/hands separation; lazily provisioned remote sandboxes as tools; vault-and-proxy credentials; per-tenant network isolation
Evaluation and benchmarkingFresh environment per task, documented resource guarantees and ceilings, pinned dependencies or mirrors

Whatever you pick, write it down, as in the policy above, so that the choice can be reviewed, versioned, and matched between evaluation and production. A sandbox policy that exists only as flags scattered through launch scripts is not a policy; it is an accident that hasn't happened yet.

In practice

This week, take one agent you run and answer five questions in writing. (1) Which point on the isolation spectrum is it actually at, not the one you assumed? (2) List every credential reachable from inside its execution environment, including files, environment variables, and mounted sockets; the target is zero. (3) What is its network policy, and where is it enforced? If the answer is "none", run it for a day behind an egress proxy in log-only mode and look at the domain list. (4) Do file tools and shell tools share one execution world, or can they disagree? (5) What are its resource limits, and would an out-of-memory kill show up in the transcript as a sandbox event or as a mysterious command failure? Fix the worst answer first; it is usually question 2.

Key takeaways

Check your understanding

1. A team's coding agent runs as the developer on their laptop. To reduce risk, they add detailed tool descriptions warning the model never to touch ~/.ssh and require approval for every shell command. Developers now approve nearly everything without reading. What is the most effective next step?

Tool descriptions shape intent, not capability. A shell tool can do anything the process can do, and a misread instruction or injected command ignores the warning.
This deepens approval fatigue rather than fixing it. The problem is that humans are the only boundary, not that there are too few of them.
Correct. The environment bounds what an approved-but-wrong action can reach, which is what makes it safe to ask fewer questions; Anthropic reported an 84% reduction in prompts from exactly this kind of isolation.
Mounting the home directory brings ~/.ssh and cloud credentials straight back into scope. The container becomes a costume over the host rather than a boundary.

2. You are designing a hosted agent that must push to customers' GitHub repositories and call their SaaS tools over MCP. Where should the GitHub token and the MCP OAuth tokens live?

Correct. This is the brain/hands separation from Managed Agents: model-generated code runs where no credentials exist, and authority is attached to specific requests by components you control outside the boundary.
Tenant isolation doesn't protect a tenant from its own agent. Any code the agent runs, including code shaped by injected content, can read environment variables and send them anywhere the network allows.
Read-only prevents modification, not reading. A token the process can read is a token the process can exfiltrate.
This puts secrets into the model's context and transcripts, where they can be echoed, logged, or leaked, which is worse than putting them in the sandbox.

3. After moving shell execution to per-task cloud VMs, an agent keeps re-applying the same edit and concluding that the test runner is broken. File-edit tools still operate on the local checkout. What is the underlying problem?

Resource limits can change outcomes, but the symptom here (edits that never seem to take effect) points to tools disagreeing about where the repository is.
The test output is accurate for the copy it ran on. Telling the model to trust it harder doesn't fix the fact that it's looking at the wrong files.
Changing isolation level doesn't address the mismatch. The same split-brain would occur with any backend if file and shell tools pointed at different machines.
Correct. This is the split-brain failure DeepSeek Harness's execution-world seam is designed to prevent: filesystem and subprocess providers move together, so Bash, PTY, and LSP follow the files.

4. Two internal harness variants differ by 2 points on a Terminal-Bench-style suite. Variant A ran on a cluster with generous memory; variant B ran with strict per-task limits. What is the best reading of the result?

Anthropic measured a 6-point swing from resource configuration alone and advised skepticism toward gaps under 3 points until configurations are matched. Two points on mismatched infrastructure is not evidence.
Correct. Limits are part of the experimental setup; they select for different agent strategies. Match them, document both the guaranteed allocation and the ceiling, then compare.
Strict limits favor a particular style (lean code, few heavy installs), not general capability. The variants weren't tested under the same conditions, so neither conclusion follows.
Anthropic found that above roughly 3x headroom, success rose faster than infra errors fell. Generous resources enable strategies that don't work under tight limits, so subtracting errors doesn't remove the confound.