PromptABCD
FeaturesLearnHow it worksUse casesFAQGuideBlogContext Blocks
Sign inGet started free
Sign inSign up
PromptABCD

A calm home for your best AI prompts. Save them once, find them in seconds, reuse them forever.

Product

  • Features
  • Chrome Extension
  • Free Courses
  • How it works
  • Use cases
  • Blog
  • Context Blocks
  • Export Anywhere
  • FAQ

Resources

  • User guide
  • Learn prompting
  • Sign in
  • Get started free

© 2026 PromptABCD. All rights reserved.

Privacy PolicyTerms and Conditions
Home/Blog/AI Harness/How to Sandbox Code Execution in Your Agent Harness
AI Harness

How to Sandbox Code Execution in Your Agent Harness

An agent code execution sandbox has to stop more than file deletion. Here's how to block network exfiltration, strip credentials, and cap resources safely.

August 28, 2026·9 min read
ShareShare
⚡Featured Prompt— copy and use right now
def run_code_tool(code: str):
    scope = {}
    exec(code, scope)          # runs in this process, full privileges
    return scope.get("result", "done")

Ask ten engineers what a code sandbox protects against and nine will say "the model deleting my files." That instinct points at the wrong threat. The quieter, more common breach is a script that quietly makes a network call — reading a secret from an environment variable and posting it somewhere you'll never see. An agent code execution sandbox that only guards the filesystem leaves the front door wide open while bolting the back one. Let's tear down a naive executor and rebuild it around the threats that actually happen.

Before: The Naive
exec()

Here's the version almost everyone writes first, because it works in the demo:

hljs python
[object Object], ,[object Object],(,[object Object],):
    scope = {}
    ,[object Object],(code, scope)          ,[object Object],
    ,[object Object], scope.get(,[object Object],, ,[object Object],)

What this does: it takes whatever Python the model wrote and runs it directly inside your own process, with your permissions, your environment variables, and your network. It returns whatever the code stashed in

result
. It's four lines, it's clean, and it's a liability the moment the model writes something you didn't anticipate.

Why It Fails

The failure isn't hypothetical, and it isn't only about malicious models. A perfectly well-meaning model, asked to "check the config," might write code that reads

os.environ
and prints your API keys into the transcript. A model debugging a network issue might write a request that hits an internal endpoint it should never reach.
exec()
grants all of it, because it runs with everything your process already has.

Four specific holes make the naive version unusable in anything real:

  • Network egress. The code can reach the internet and your internal network. This is the exfiltration path most people forget, and it's the one that leaks secrets.
  • Ambient credentials. Environment variables, mounted cloud credentials, and open database connections are all visible to
    exec()
    . The model didn't need to break in — you handed it the keys.
  • No resource ceiling. A
    while True:
    or a runaway allocation freezes or crashes your host. LLMs write infinite loops more often than you'd hope.
  • Shared process state. Code running via
    exec()
    can reach into your harness's own variables and corrupt the run itself.

⚠️ Common mistake: Treating a code sandbox as purely a filesystem problem. Blocking file writes while leaving network access open is the security equivalent of locking the windows and propping the door. The three things that matter most — no network, no ambient credentials, hard resource limits — have nothing to do with the filesystem at all.

After: The Sandboxed Executor

The fix moves execution out of your process and into a disposable, locked-down environment. A container is the pragmatic default:

hljs python
[object Object], subprocess, tempfile, os

,[object Object], ,[object Object],(,[object Object],):
    ,[object Object], tempfile.TemporaryDirectory() ,[object Object], work:
        path = os.path.join(work, ,[object Object],)
        ,[object Object], ,[object Object],(path, ,[object Object],) ,[object Object], f:
            f.write(code)
        ,[object Object],:
            out = subprocess.run(
                [,[object Object],, ,[object Object],, ,[object Object],,
                 ,[object Object],, ,[object Object],,          ,[object Object],
                 ,[object Object],, ,[object Object],,            ,[object Object],
                 ,[object Object],, ,[object Object],,               ,[object Object],
                 ,[object Object],, ,[object Object],,          ,[object Object],
                 ,[object Object],,                 ,[object Object],
                 ,[object Object],, ,[object Object],,
                 ,[object Object],, ,[object Object],,      ,[object Object],
                 ,[object Object],, ,[object Object],, ,[object Object],],
                capture_output=,[object Object],, text=,[object Object],, timeout=timeout)
            ,[object Object], (out.stdout ,[object Object], out.stderr)[:,[object Object],]
        ,[object Object], subprocess.TimeoutExpired:
            ,[object Object], ,[object Object],

What this does: it writes the model's code to a throwaway directory, runs it inside a container with no network, capped memory and CPU, a process-count limit, a read-only root filesystem, and one small writable scratch space — then kills it on a timeout. Every credential in your host process is invisible to it. When the container exits, everything the code touched vanishes with it.

⚡ Pro tip:

--network none
is the single highest-value flag on that command. If your code genuinely needs to fetch something, add a narrow proxy allowlist rather than opening egress wholesale. Default-deny on the network catches the exfiltration class of bug that filesystem controls can't see.

Breaking Down the Sandbox Layers

Each flag maps to a specific threat, and understanding the mapping lets you tune the sandbox for your situation instead of copying it blindly.

Isolation of credentials comes from running in a fresh container that never inherited your environment. This is why "run it in a subprocess" isn't enough — a subprocess still sees your environment variables. The container starts clean.

Resource ceilings —

--memory
,
--cpus
,
--pids-limit
— turn a denial-of-service into a clean failure. The fork bomb that would take down your host instead hits the process limit and dies with a readable error the model can learn from.

Filesystem containment comes from

--read-only
plus a single mounted scratch directory. The code can write where you allow and nowhere else, and even that scratch space evaporates when the container exits.

Privilege containment comes from

no-new-privileges
, which stops the code from escalating even if it finds a
setuid
binary inside the image.

For higher-assurance needs, containers aren't the ceiling. Micro-VMs like Firecracker or a gVisor runtime give you kernel-level isolation instead of shared-kernel namespaces — heavier to operate, but the right call when the code is truly untrusted.

⚡ Pro tip: Pin the image to a digest, not the

:latest
tag. An agent sandbox that silently pulls a new base image is a supply-chain surprise waiting to happen.
python:3.12-slim@sha256:...
guarantees the environment your code runs in is the one you audited.

Testing That the Sandbox Actually Holds

A sandbox you haven't attacked is a sandbox you don't trust yet. The difference between an agent code execution sandbox that works and one that only looks like it works shows up when you point adversarial code at it on purpose. Write the escape attempts yourself, before a model writes them by accident:

hljs python
ESCAPE_TESTS = {
    ,[object Object],:   ,[object Object],,
    ,[object Object],:   ,[object Object],,
    ,[object Object],: ,[object Object],,
    ,[object Object],:  ,[object Object],,
    ,[object Object],:  ,[object Object],,
}

,[object Object], ,[object Object],(,[object Object],):
    ,[object Object], name, code ,[object Object], ESCAPE_TESTS.items():
        out = runner(code)
        blocked = ,[object Object], ,[object Object], out ,[object Object], ,[object Object], ,[object Object], out.lower() ,[object Object], ,[object Object], ,[object Object], out
        ,[object Object],(,[object Object],)

What this does: it runs five representative attacks — a network call, an environment dump, a fork bomb, a CPU spin, and a write outside the scratch dir — and reports which ones the sandbox stopped. Every line should print

BLOCKED
. The moment one prints
ESCAPED
, you've found a real hole, and you've found it in a test instead of in an incident report.

Run this audit in your CI pipeline, not just once by hand. Base images change, Docker flags get dropped in a refactor, and a sandbox that held last month can quietly spring a leak. An automated audit catches the regression the day it happens rather than the day it matters.

Three teams that gained real confidence from this:

  • A DevOps engineer wires the audit into the deploy pipeline, so any change to the container config that reopens network egress fails the build before it ships.
  • A fintech platform engineer extends the escape tests with attempts to reach the internal metadata endpoint that leaks cloud credentials, proving that specific exfiltration path is closed.
  • A university research group running student-submitted agents runs the full audit per submission, treating every unaudited sandbox as untrusted by policy.

⚡ Pro tip: Keep the escape tests in the same repo as the sandbox and treat a new escape technique like a new bug — add a test that reproduces it, then fix until the test goes green. Over time your

ESCAPE_TESTS
dictionary becomes an institutional memory of every way someone tried to break out, and a guarantee none of them work anymore.

Variations for Different Threat Models

The right sandbox depends on how much you trust the code's source, and matching effort to threat keeps you from over-building.

  • A data science team running model-written pandas snippets against non-sensitive sample data can stay on the container setup above — no network, capped resources, disposable. The threat is mostly runaway loops, not exfiltration.
  • A fintech platform engineer whose agent generates code that touches anything near real customer data should move to micro-VM isolation and route the one permitted outbound call through an audited proxy. Here the exfiltration threat is the whole game.
  • A security research group analyzing genuinely untrusted code treats every execution as hostile: micro-VM, zero network, no shared kernel, and a fresh VM per run so nothing carries between executions.

Same core idea, three levels of paranoia — chosen by what happens if the containment fails, not by what feels rigorous.

There's a cost dimension worth weighing too. A fresh micro-VM per execution is the safest option and also the slowest to start and the most expensive to run at scale. If your agent runs thousands of snippets a day and the code comes from a source you partly trust, a warm pool of pre-started containers gives you most of the isolation at a fraction of the per-run cost. The right answer balances the blast radius of an escape against the throughput you actually need — a research tool analyzing malware and a data agent running vetted pandas snippets sit at opposite ends of that trade, and copying one's setup onto the other either wastes money or takes on risk you didn't mean to.

Save and Reuse This

The move that makes an agent code execution sandbox actually safe is separating two decisions your naive

exec()
collapsed into one: whether to run code and how to run it. Route every execution through the same locked-down path, default-deny the network, strip credentials by starting clean, and cap resources so a bad loop fails instead of crashing. The container command above is a copy-paste starting point; harden it toward micro-VMs as your threat model demands.

There's one more piece worth keeping: the system prompt and tool description that tell the model what the sandbox can and can't do. A model that knows it has no network access stops writing code that assumes network access, which cuts your failed-run count sharply. Those hard-won descriptions — "you run in a sandbox with no internet, 256MB RAM, and a 10-second limit" — belong in a prompt library like PromptABCD, tagged to the sandbox they describe, so every new code agent inherits the guardrails you already worked out instead of learning them by crashing.

agent code execution sandboxdockersandboxingai agentssecuritycode execution

Continue Reading

The SWE-bench Harness Explained for Agent Builders
AI Harness

The SWE-bench Harness Explained for Agent Builders

The swe-bench harness fails logically correct patches when the environment is wrong. Learn how it grades, what FAIL_TO_PASS means, and how to run it yourself.

August 28, 2026·8 min read
Building an Evaluation Harness for Your Agent
AI Harness

Building an Evaluation Harness for Your Agent

The best way to build agent eval harness infrastructure isn't LLM-as-judge. Learn to design verifiable tasks and programmatic graders you can actually trust.

August 28, 2026·8 min read
What Is an Eval Harness, and Why Do Agents Need One?
AI Harness

What Is an Eval Harness, and Why Do Agents Need One?

An AI eval harness tells you your agent works across a hundred tasks, not just the one you tried. Learn what it measures and why agents need it more than models.

August 28, 2026·8 min read

Save the prompts from this post

PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.

Start free →
← PreviousTool Routing Inside an AI Harness: A Practical GuideNext →Building a Safe Shell Tool for Your Agent Harness
Share this post:
ShareShare