Agent Execution Environments: Filesystems, Shells, Browsers, Code, Databases, and Sandboxes
The demo worked because the agent was running on your laptop. It had your credentials, your network, your home directory, and your shell history. Move it…

Key topics
The demo worked because the agent was running on your laptop. It had your credentials, your network, your home directory, and your shell history. Move it into a container with a read-only mount, no egress, and a different user, and the same task fails in ways the traceback will not explain.
That gap is the subject of this article. Not "should the agent have tools" — you are past that — but where the tool actually executes, as whom, for how long, and what survives the run.
The Execution Surface Is the Real Permission Boundary
A tool is a name the model can emit. An execution surface is the thing that runs when the name is emitted. These are not the same object, and conflating them is the most common source of agent security bugs I see.
A run_shell tool and a run_python tool can share one surface — same container, same user, same filesystem, same network — or they can be two entirely different surfaces with different identities and different blast radii. The model sees two function schemas. The system sees one or two capability grants. The grant is what matters.
Every surface answers five questions, whether or not you answered them deliberately:
| Axis | Question | Failure if unanswered |
|---|---|---|
| Identity | Who acts when the tool runs? | Confused deputy; agent inherits user authority |
| Reach | What can it read, write, execute, reach over network? | Exfiltration, lateral movement |
| Lifetime | Per-call, per-session, per-task? | Orphaned environments, stale credentials |
| Resource envelope | CPU, memory, disk, wall clock, concurrency? | Fork bombs, cost blowups, lock contention |
| Artifact behavior | What persists, where, under what name? | Lost results, silent truncation |
Two integration patterns dominate deployed systems. In the agent-in-a-sandbox model, the agent process itself runs inside the boundary. In the agent-with-a-sandbox model, the agent stays outside and drives the boundary through an interface — executing commands, manipulating files remotely. Both are common, and they fail differently. Agent-in-a-sandbox tends to fail through escape and resource exhaustion. Agent-with-a-sandbox tends to fail through over-broad interface design: the remote-control API becomes the real attack surface, and it is usually less audited than the sandbox itself.
The model's intent is untrusted input. The surface is the only thing that makes that input safe or unsafe. If you cannot describe the surface in terms of identity, reach, lifetime, envelope, and artifacts, you have not designed it — you have inherited it.
Knowledge check
Check your understanding
Answer this question before you continue.
A Worked Scenario: The Five Axes in Motion
Abstract axes are easy to nod at and hard to apply. Take one task and run it through the model.
Task: "Update the dependency in service-a, run its test suite, and publish a summary report."
The naive implementation grants one shell surface with the developer's credentials, a writable repo mount, open network, and no timeout. That is four capability grants disguised as one tool. Decompose it by required effect instead:
| Effect | Surface | Identity | Reach | Lifetime | Envelope | Artifact |
|---|---|---|---|---|---|---|
| Read repo, edit manifest | Filesystem (repo mount, read-write) | Agent principal | Repo path only | Task-scoped | Disk cap | Diff, committed branch |
| Fetch dependency | Network (package index) | Agent principal | Allow-listed index | Per-call | Bandwidth cap | Lockfile |
| Run tests | Code execution | Agent principal | Repo + runtime | Task-scoped | CPU, memory, wall clock | Test log, exit code |
| Publish report | Object store (write) | Report-writer principal | One bucket prefix | Per-call | Size cap | Report object, deterministic name |
Four surfaces, three identities, one task. The dependency fetch cannot reach the object store. The test runner cannot reach the package index. The report writer cannot touch the repo. Each boundary is small enough to reason about, and each failure is contained to one row.
This is the synthesis the five-axis table asks you to perform. Do it once per task class, not once per tool call.
Filesystem and Shell: The General-Purpose Surface
The filesystem became the default harness primitive for three reasons that compound: it offloads state out of context, it gives the agent a workspace to read and write, and it acts as a coordination surface for multiple agents and humans. Git adds versioning on top, which turns the filesystem into a rollback mechanism — branch an experiment, diff the agent's work, revert the bad turn.
That leverage is exactly why it is the highest-risk surface. A shell with filesystem access is a general-purpose capability. You cannot enumerate what it can do.
Mount strategy is the first control, and it is more effective than command filtering. Read-only roots for reference material. A per-task scratch directory for writes. Hard separation between the agent's workspace and host paths, credential stores, and anything under a home directory. If the agent can cat ~/.aws/credentials, no amount of prompt discipline will save you.
The shell-versus-allow-list tradeoff is real and usually resolved badly. Allow-listed commands are auditable and predictable, and they rot. Tasks diversify, the list grows, someone adds bash -c to unblock a demo, and the allow-list becomes decoration. My preference: give the agent a real shell inside a tightly bounded surface, and put the control at the mount, network, and identity layer where it does not rot. Reserve allow-lists for the narrow case where the command set is genuinely fixed.
Failure modes worth naming:
- Path traversal out of the workspace via
..or absolute paths. - Symlink escapes — a symlink inside the workspace pointing at
/etcor a mounted secret. - Unbounded disk growth from logs, caches, or a loop writing files.
- Destructive commands that succeed because nothing in the surface said no.
rm -rfinside a writable mount does exactly what it says.
Git helps with recovery, not prevention. Treat it as the undo button, not the seatbelt.
Code Execution and Language Runtimes
Code execution has become the default general-purpose strategy for autonomous problem solving, and the reason is straightforward: a narrow pre-built tool solves the problem you anticipated, while a code interpreter solves the problem the agent discovers. When the task is exploratory, the interpreter wins.
The surface question is what the interpreter can reach. A code-execution surface that shares a filesystem and network with the shell surface is not a separate boundary — it is the same boundary with a different entry point. In the worked scenario, the test runner is a distinct surface precisely because it does not share the package-index reach of the fetch step.
Runtime provisioning matters more than it looks. Pre-install language runtimes, package managers, and common CLIs so the agent does not spend its budget bootstrapping an environment on every task. This is a harness responsibility, not a model responsibility; the model will happily spend twenty turns installing pip if you let it.
Dependency installation is where reproducibility and supply chain meet. Pinning environments and caching layers is the practical answer. Installing at task time is slower, less reproducible, and pulls arbitrary packages from the network under the agent's identity. If you must install at task time, do it inside the sandbox with a restricted index and a pinned lockfile.
Bound the output. Unbounded stdout is both a context hazard and a memory hazard — a runaway loop printing to stdout will fill your capture buffer and then your context window. Truncate with an explicit marker so the model knows it is seeing a prefix, not the whole result.
# Sketch: bounded execution with explicit truncation
MAX_OUTPUT_BYTES = 64_000
def run_bounded(code: str, timeout_s: int = 30) -> dict:
result = sandbox.exec(code, timeout=timeout_s)
stdout = result.stdout
truncated = len(stdout) > MAX_OUTPUT_BYTES
return {
"stdout": stdout[:MAX_OUTPUT_BYTES],
"truncated": truncated,
"exit_code": result.exit_code,
"timed_out": result.timed_out,
}
The truncated flag is not cosmetic. Without it, the model reasons over a partial result as if it were complete.
Failure modes: fork bombs, runaway loops, memory exhaustion, and code that writes outside its intended directory. Timeouts and memory caps catch most of these; the ones that slip through usually do so because the limit was set per-call rather than per-environment.
Knowledge check
Check your understanding
Answer this question before you continue.
The Execution Contract at the Surface Boundary
The five axes only become enforceable when the surface returns a structured result. A free-form string return collapses identity, limits, and artifact state into prose the model has to guess at. Make the contract explicit:
{
"surface_id": "code-exec-test-runner",
"principal": "agent-principal",
"reach": {"paths": ["/workspace/service-a"], "domains": []},
"limits": {"cpu_s": 120, "mem_mb": 2048, "wall_s": 300},
"result": {
"exit_code": 1,
"stdout": "...",
"truncated": false,
"timed_out": false
},
"artifacts": [
{"name": "test-log", "uri": "s3://reports/service-a/test-log.txt", "sha256": "..."}
]
}
The contract does three jobs. It tells the model what surface it actually touched, so a retry targets the same boundary. It exposes truncation and timeout as first-class fields, so the model can distinguish "no output" from "output I did not receive." And it names artifacts with a stable URI and hash, so a later run can retrieve them after the sandbox is gone.
Keep the contract implementation-neutral. The point is not the schema; the point is that identity, reach, limits, and artifacts cross the boundary as data, not as assumptions.
Browsers and Network Reach
A browser surface grants something none of the others do: authenticated sessions on third-party systems. Cookies, form submission, the ability to act as the user. That is a fundamentally different authority level than a filesystem mount.
Network egress is the quiet exfiltration channel. A sandbox with open network access is half a boundary — the agent can read a secret from the filesystem and POST it anywhere. Default-deny egress with an allow-list of domains is the posture that actually holds. The allow-list is annoying to maintain and it is the difference between a contained incident and a breach.
The browser is also an untrusted input channel. Fetched content can contain instructions, and the model does not reliably distinguish data from directives. This is the interaction that makes browser agents hard: the same surface that lets the agent act on the web lets the web act on the agent. Prompt injection through fetched content is not a bug you patch; it is a property of the surface you constrain.
Session and credential handling follows from that. Scoped tokens over long-lived user sessions. Short-lived credentials that expire before the run finishes. Never let the agent's browser session carry the same authority as the human's.
Failure modes: credentials leaking into logs or screenshots, unintended writes or purchases on external systems, and content that redirects the agent's goal mid-task.
Databases and Stateful Backends
The database surface is different in kind from the filesystem. It is shared, concurrent, and often production-adjacent. Mistakes are durable and visible to other people.
The default posture is read-only replicas, scoped roles, and least-privilege credentials. For analytical workloads, schema-level and row-level scoping beats query allow-lists — the scoping is enforced by the database, not by a parser you wrote. Allow-lists still earn their place for a small set of known write operations.
Make writes reversible or at least detectable. Transactions, idempotency keys, and dry-run modes. A dry-run that returns the affected row count before the real update is cheap insurance.
Keep the agent's own working state out of the database it is mutating. Checkpoints, scratch tables, and intermediate results belong in a separate store. If the agent corrupts its own checkpoint table mid-task, you have lost both the work and the recovery path.
Failure modes: accidental mass updates from a missing WHERE, lock contention from long-running agent queries, and cost blowups from unbounded scans against a warehouse that bills by bytes read.
Knowledge check
Check your understanding
Answer this question before you continue.
Isolation Models: Containers, MicroVMs, and Process Boundaries
Before comparing implementations, fix the axis. Isolation strength is a function of kernel sharing, startup latency, snapshot and restore support, and how much of a real OS interface the agent gets.
The choice is derived from the threat model, not from preference. Ask three questions: what is untrusted (model-authored code, fetched content, or only validated arguments?), what must be shared (a repo mount, a credential, a network destination?), and does persistence or low latency dominate? In the worked scenario, the test runner executes model-authored code and touches the network, so it needs a stronger boundary than the report writer, which only writes to one bucket prefix.
Containers are the common default: fast, familiar, and sharing a kernel with the host. That last property is the whole story. A container escape is a host compromise. MicroVMs put a separate kernel between the agent and the host at the cost of higher startup and operational overhead. For code you did not write and cannot fully predict, the stronger boundary is usually worth the latency.
Snapshot and restore deserve first-class treatment. Starting from a prepared image means the agent does not reinstall tooling every run. Checkpointing filesystem or process state at turn boundaries means long tasks survive interruption. The mechanism is not exotic — filesystem snapshots plus process checkpointing — but the design decision is: what granularity of checkpoint do you need, and what does it cost to take one?
Ephemeral versus long-lived is the other axis. On-demand creation and teardown scales and isolates cleanly. Persistent environments give continuity and accumulate state you have to reason about. Most production systems want ephemeral by default with an explicit path to persistence.
When is isolation overkill? Short, read-only, deterministic tasks that never execute model-authored code. If the agent is calling a fixed API with validated arguments, you do not need a microVM. You need input validation.
Knowledge check
Check your understanding
Answer this question before you continue.
Identity, Authorization, and Approval Gates
Whose identity does the agent use? Three answers, with different consequences:
- Delegated user identity. The agent acts as the end user. Convenient, and it means every mistake is attributed to a real person with real permissions.
- Dedicated agent identity. The agent has its own principal with its own scopes. Auditable, and it requires you to actually provision and rotate credentials.
- Ambient service account. The agent inherits whatever the service runs as. Easiest, and the hardest to reason about because the authority is implicit.
Scope credentials per surface, not per agent. The browser token, the database role, and the filesystem mount should not share one authority. If they do, a compromise in the weakest surface grants the strongest. In the worked scenario, the report writer's identity cannot touch the repo, so a compromise of the publish step cannot rewrite code.
Approval gates are where policy becomes visible. Default-require approval for sensitive operations, auto-approve only operations you have verified are safe, and log enough that the approval means something. The failure mode here is approval fatigue: gate too much, and the human clicks approve without reading. A gate that is always approved is not a gate.
Treat skills, scripts, and MCP-provided capabilities as third-party code. Read the content before deploying. Verify that a script's actual behavior matches its stated intent. This is the same lesson we learned with signed drivers — code that loads into a privileged context needs review, and the ecosystem around agent capabilities has not yet internalized it.
Failure modes: confused-deputy problems where the agent uses its authority on behalf of a less-privileged caller, over-broad tokens reused across surfaces, and approval gates that have become rubber stamps.
Resource Limits, Observability, and Recovery
A surface without limits, logs, and a recovery path is not production-ready, regardless of how well it is isolated.
Set the envelope explicitly: CPU, memory, disk, wall-clock timeout, and concurrency cap per environment. Decide what happens when a limit is hit — hard kill, graceful signal, or queue. The default of "the process runs until it finishes" is how you get a runaway job that bills for a week.
Observability is a design requirement, not a logging afterthought. Record which surface was invoked, with what arguments, under which identity, and what came back. Without that, you cannot answer the only question that matters during an incident: what did the agent actually do?
Artifact handling is the part teams skip and regret. Decide what the agent produces, where it is stored, how it is named, and how it is retrieved after the environment is torn down. An artifact that lives only inside an ephemeral sandbox vanishes with it. Name artifacts deterministically so a later run can find them — the sha256 and stable URI in the execution contract exist for exactly this reason.
Recovery comes down to retry versus resume. Retry replays the whole task; resume picks up from a checkpoint. Which is safe depends on whether the task's effects are idempotent. A read-only analysis is safe to replay. A payment is not. Decide per task, not per system.
Failure modes: silent truncation of results the model treats as complete, orphaned environments consuming resources after the run ends, and artifacts that disappear with the sandbox.
Choosing Surfaces: A Decision Procedure
Here is the procedure I use, and it is deliberately boring.
- Start from required effects, not available tools. List what the task must read, write, execute, or reach. Not "the agent needs a shell" — "the agent must read three files, run a test suite, and write a report."
- Grant the narrowest surface that produces those effects. Widen only when a concrete task fails, and record why.
- Match isolation strength to trust. Model-authored code that touches the network gets a stronger boundary than a fixed API call with validated arguments.
- Decide persistence and artifact policy before the first run. Not after the first lost result.
- When not to build. If a managed sandbox already provides the isolation, lifecycle, and snapshot behavior you need, spend the engineering effort on policy and evaluation instead. Building a sandbox is not the interesting problem; deciding what the agent is allowed to do inside it is.
The decision rule that ties it together: for any new agent task, write down the required effects, the identity it acts under, the lifetime of the environment, and what must survive teardown. Grant only that. Everything else is a capability you did not intend to give away.
Verify the Boundary, Do Not Just Describe It
An audit that produces a document is not evidence. The boundary holds or it does not, and the only way to know is to try to cross it. Take one existing agent tool and run this verification loop against its surface:
- Out-of-scope path. Ask the agent to read a file outside its mount. Expect a permission error, not a partial read.
- Denied network destination. Ask it to reach a domain not on the allow-list. Expect a connection failure, not a timeout that looks like a slow success.
- Oversized output. Trigger a command that prints more than the truncation limit. Confirm the
truncatedflag is set and the model sees it. - Expired credential. Let a scoped token expire mid-run. Confirm the failure is a clean auth error, not a silent fallback to ambient authority.
- Timeout. Run a task that exceeds the wall clock. Confirm the environment is killed and the kill is recorded.
- Teardown and retrieval. Tear down the environment, then retrieve the artifact by its deterministic name. If it is gone, your artifact policy is a hope, not a control.
Record the observed result for each. These are boundary tests, not a security-completeness guarantee — they prove the controls you designed are actually wired up, and they surface the gap between the surface you described and the surface that runs. That gap is where incidents live. Close it before you widen the surface.
Knowledge check
Final check
Finish the article by checking the ideas you just learned.
References
Research updated Sep 11, 2026


