In short

Assume the code an agent writes was written by an attacker, because any page, file or tool result the model read may have steered it. Run it in a fresh, disposable sandbox: a container hardened with no network, a read-only root, no capabilities, a non-root user and limits on memory, CPU, processes and time, or gVisor or a microVM when the code is less trusted still. Mount nothing from the host and pass no secrets in. Fail closed when the sandbox is unavailable. Keep side effects outside it, behind an approval that shows the exact action; for computer-use agents, treat the screen as untrusted and keep logins and irreversible clicks with a person.

  • OWASP lists this as ASI05:2026 Unexpected Code Execution (RCE) in the Top 10 for Agentic Applications; its mitigations start with never running as root and running code in sandboxed containers with strict limits, including on network access.
  • In-process restrictions are not a sandbox: Python's own documentation says audit hooks are not suitable for one, and a regex that blocks import os is bypassed by the next spelling.
  • A container shares the host kernel. Harden it with --network=none, --read-only, --cap-drop=ALL, no-new-privileges, --pids-limit, equal --memory and --memory-swap, and --cpus; use gVisor (--runtime=runsc) or a Firecracker microVM for a stronger boundary.
  • Files go in and out as data, never as a shared folder: Auto-GPT's writable compose-file mount (CVE-2023-37273) let generated code escape to the host on the next restart.
  • Never fall back to running the code on the host when the sandbox fails, and never put API keys, tokens or credential files inside it.
  • Computer-use vendors agree: run the agent in an isolated VM, container or browser profile, allowlist the sites it may visit, keep sensitive credentials away from it, and confirm purchases, messages, deletions and agreements with a person.

Verify it

  1. Run the agent-code-sandbox project's live tests against your sandbox image and flags: every escape case must fail and every legitimate case must pass.
  2. Inspect the docker run command your tool actually issues and confirm it has --network=none, --read-only, --cap-drop=ALL, no-new-privileges, --pids-limit, equal --memory and --memory-swap, --cpus and no --volume, --mount, --env or Docker socket.
  3. Stop the Docker daemon (or point the tool at a missing binary) and confirm the tool raises an error instead of running the code anywhere else.
  4. Run an endless loop with a short deadline and confirm docker ps -a shows no container afterwards.
  5. For a computer-use agent, serve a page with an injected instruction and a fake login form from an allowlisted test domain, and confirm the agent asks before acting and never types a credential.
Applies to
  • Docker Engine 29.x (tested with 29.4.3, runc 1.3.5, cgroup v2)
  • Sandbox image python 3.14.8-slim-trixie, pinned by digest
  • Computer use tools Anthropic computer use toolset, OpenAI computer tool and Gemini computer use, as documented on 11 October 2026

Agents and features that execute model-generated Python, JavaScript or shell, and agents that operate a browser or desktop through screenshots and input events. Coding assistants on a developer's own machine are covered in the AI-assisted development guide.

Fix it in your stack

Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.

Learning goals and prerequisitesAbout 45 minutes: 15 to read and 30 for the exercise, plus 45 to 75 for the local project

What you will learn

  • Choose an isolation boundary (hardened container, gVisor, microVM, WebAssembly, separate account) for what the generated code must do.
  • Run generated code with no network, no secrets, no host mounts and hard limits on time, memory, CPU, processes and output.
  • Keep side effects outside the sandbox and behind an approval that shows the exact action.
  • Treat a computer-use agent's screen as untrusted input, and keep credential entry and irreversible clicks with a person.

Before you start

Developers and platform engineers who give an LLM agent a code interpreter, a shell, or control of a browser or desktop

  • Know what a container shares with its host (the kernel) and what docker run flags control.
  • Know how an agent's tool calls reach your code.
  • Python 3.11 or later and Docker for the local project.

Refresh first: Contain prompt injection at the application boundary · Authorize agent tools outside the model

Go to the guided practice

One run of a code-execution tool

No network, no secretsModelTool hostSandbox containerApprovergenerated code1job on stdin2result envelope3checked, capped output4exact action to approve5Trust boundaryUntrusted input1Step, described below
  1. The model proposes codeThe code may have been steered by anything the model read, so the tool host treats it as hostile.
  2. The host sends a jobCode and input files travel on stdin to a fresh container with no network, no mounts, no secrets and hard limits.
  3. The sandbox returns a resultExit status, capped stdout and stderr and any output files, as data the code could have forged.
  4. The host checks and caps itNames, sizes and encoding are validated before anything goes back to the model.
  5. Side effects wait for a personIf the result proposes an action outside the sandbox, the approver sees the exact action and arguments.
The sandbox computes; it never holds credentials or reaches other systems. Anything that changes the world goes through the tool gateway and an approval.

Treat generated code as an attacker's code

An agent that writes and runs code turns text into execution. The model cannot reliably tell instructions from data, so a web page, document, email or tool result it read can steer what it writes, and the code then runs with whatever the runtime can reach. The OWASP Top 10 for Agentic Applications 2026 lists this as ASI05 Unexpected Code Execution (RCE): generated scripts, shell commands, deserialized objects or template evaluation that lead to host or container compromise, persistence or sandbox escape. Its examples include prompt injection that runs attacker-defined code and unverified package installs that execute during installation.

Auto-GPT showed how quickly this reaches the host. Its own advisories describe two flaws fixed in version 0.4.3. In CVE-2023-37273, the docker-compose.yml in the repository root mounted itself into the container without write protection, so code run through the execute_python_file or execute_python_code commands could overwrite it and gain control of the host the next time Auto-GPT started. In CVE-2023-37274, which affected the setup that runs Auto-GPT on the host and only the generated code in a container, execute_python_code wrote model-supplied code to a file with a model-supplied name it did not sanitize, so a name such as ../../../main.py overwrote Auto-GPT's own code, which then ran on the host at the next start. Positive Security's write-up reached those commands through an indirect prompt injection on a web page the agent browsed.

Nothing inside the interpreter fixes this. Python's documentation says audit hooks "are not suitable for implementing a sandbox" and that malicious code can disable or bypass them; denylists of imports or shell commands fail the same way. The boundary has to be the operating system or a hypervisor, and it has to assume the code is hostile on every run.

Choose the isolation boundary by what the code must do

Pick the weakest boundary that still contains the code you expect, then add one level if many tenants share the machine or nobody reviews what runs. Whatever you choose, run one disposable environment per job or per session, and never reuse one across users.

Choose the isolation boundary by what the code must do
OptionWhat isolates the codeGood forWatch for
Hardened container (runc)Linux namespaces, cgroups, capabilities and Docker's default seccomp profile, which disables around 44 of 300+ system callsData analysis and scripts for one tenant at a timeIt shares the host kernel, so a kernel or runtime bug is an escape; the Docker daemon runs as root and must not be reachable from the sandbox
gVisor (runsc)An application kernel that implements a Linux-like interface, so the code's system calls are handled by gVisor rather than passed straight to the host kernelUntrusted code from many users on shared hostsIt implements a Linux-like interface, not Linux itself, so test your workload under it
MicroVM (Firecracker, Kata Containers)A KVM virtual machine per job with a minimal device model; Firecracker emulates five devices and adds a jailer as a second line of defenseMulti-tenant code interpreters and anything that needs a full kernelNeeds KVM, so it runs on bare metal or nested virtualization, not inside most containers
WebAssembly (Wasmtime with WASI, Pyodide)The Wasm sandbox: the code reaches only the imports and directories it is givenPure computation in a browser or an edge functionPyodide's threading, multiprocessing and sockets do not work, so many libraries do not either
Separate OS account or VM per userOrdinary operating system permissionsLong-lived workspaces such as a hosted notebookEasy to grant too much: shared folders, credentials in the home directory, an open network
Hosted code execution toolThe provider's sandboxAnalysis where data may leave your infrastructureCheck what it allows: Anthropic's has no internet access and 5 GiB of memory, OpenAI's Code Interpreter container expires after 20 idle minutes, and Gemini's code execution stops after 30 seconds

Lock the container down

The function below builds the docker run command for one job in the agent-code-sandbox project, whose live tests run each flag against real escape attempts. The image is pinned by digest. --network=none leaves only the loopback device. --read-only and a size-limited tmpfs give the code scratch space but no way to change the image. The code runs as nobody with no capabilities, and no-new-privileges stops a setuid binary from raising them again. --pids-limit ends a fork bomb, equal --memory and --memory-swap mean the kernel kills the code at the limit instead of swapping, --cpus caps its share of the machine and --ulimit=fsize caps any file it writes. --log-driver=none keeps its output out of Docker's logs, and --init reaps any processes it leaves.

What is absent matters as much: no --volume or --mount, no --env, no --privileged, no --network=host or --pid=host, no seccomp=unconfined and above all no Docker socket. Docker's own security page says only trusted users should control the daemon, because it runs as root; a container that can reach it can start a privileged container on the host. For a stronger boundary, keep the same flags and add --runtime=runsc once gVisor is installed.

Fixedreference.pyPython
def sandbox_argv(image: str, name: str, limits: Limits = Limits(), docker=("docker",)) -> list:
    """`docker run` for one job: a fresh container with no network and nothing from the host."""
    if not DIGEST_PINNED.fullmatch(image):
        raise ValueError("pin the sandbox image by digest")
    return [
        *docker, "run", "--rm", "-i", "--init", "--name", name,
        "--network=none",
        "--read-only",
        f"--tmpfs=/tmp:rw,nosuid,nodev,noexec,size={limits.tmpfs_bytes}",
        f"--user={USER}",
        "--cap-drop=ALL",
        "--security-opt=no-new-privileges",
        f"--pids-limit={limits.pids}",
        f"--memory={limits.memory}",
        f"--memory-swap={limits.memory}",  # equal to --memory: no swap
        f"--cpus={limits.cpus}",
        f"--ulimit=fsize={limits.file_bytes}",
        "--ulimit=nofile=256:256",
        "--ulimit=core=0",
        "--log-driver=none",  # the code's output must not land in Docker's logs
        image, "python", "-I", "-c", RUNNER,
    ]

Pass files in and out as data, not as a shared folder

A shared folder is the most common way out of a code sandbox. If the host mounts a job folder read-write and then reads the code's output files from it, the code can leave a symbolic link named report.txt that points at /home/agent/.env, and the host follows it and hands the agent's API key back to the model. In the project's live tests the starter does exactly this on Docker Desktop. Auto-GPT's compose file shows the other direction: a writable mount lets the code change something the host will later execute.

Send inputs on stdin as part of the job, write them to the container's tmpfs, and have a small runner inside the container return the exit status, stdout, stderr and any regular files from an output folder as one JSON envelope on stdout. The runner opens files with O_NOFOLLOW and skips links, but the generated code runs as the same user and can forge the envelope, so the host checks it again: valid JSON of the expected shape, file names from a strict pattern, sizes under a limit, base64 that decodes. Nothing the code returns becomes a path on the host until those checks pass.

Keep the network off, or name the destinations

With network access, generated code can send whatever it finds to an attacker, reach your internal services and cloud metadata endpoint, or download a second stage. Start with no network at all. That also stops pip install, which is a feature: build an image with the packages the tool supports, pin it by digest and update it on a schedule, so a hallucinated or typosquatted package name cannot be installed at run time.

If the code genuinely needs a service, do not give the container a general route out. Attach it to an internal network whose only exit is an egress proxy with an allowlist of host names (ASVS 5.0 V13.2.4), block the link-local metadata address, and prefer having the host call the API on the code's behalf through a narrow tool so the credential never enters the sandbox. Remember that DNS is a channel too: a resolver that answers any name lets code leak data in the names it looks up.

Keep secrets out of the sandbox, and fail closed

The sandbox should hold nothing worth stealing: no model API key, no cloud credentials, no repository tokens, no SSH keys, no .env file and no environment variables copied from the agent. Docker passes no host environment variables unless you ask for them, so the risk is the flag someone adds later; test for it by planting a canary variable on the host and checking the code cannot print it. If the code must act on a system, give the host a narrow, authorized tool for that action instead of handing the code a token.

Decide in advance what happens when the sandbox cannot start. A tool that falls back to subprocess.run([sys.executable, "-c", code]) "so the agent keeps working on laptops" runs hostile code with the agent's environment, files and network. Raise an error and let the agent tell the user the tool is unavailable.

Bound time, memory, CPU, processes and output

Generated code loops, allocates and forks by mistake as well as by design, and every run costs you capacity. Set each limit in the runtime, not in the prompt: processes with --pids-limit, memory with equal --memory and --memory-swap, CPU share with --cpus, file size with --ulimit=fsize, and wall-clock time with a deadline on the host. The deadline must kill the container itself: docker run is only a client, so a timeout that kills it leaves the container running. The excerpt below kills the container by name and then removes it in case the client died before the container started.

Cap what goes back to the model as well. Output becomes part of the next prompt, so a script that prints 30 MB turns into an unbounded-consumption problem (LLM06:2026) and a channel for injected instructions. Return the first few kilobytes and say the rest was cut.

Fixedreference.pyPython
try:
    status = proc.wait(timeout=limits.timeout_seconds)
except subprocess.TimeoutExpired:
    # Killing the docker client does not stop the container: kill the container by name.
    subprocess.run([*docker, "kill", name], capture_output=True, timeout=30)
    proc.kill()
    proc.wait()
    # If the client died before the container started, remove the late container too.
    subprocess.run([*docker, "rm", "--force", name], capture_output=True, timeout=30)
    raise Rejected(f"timed out after {limits.timeout_seconds:g} s; container killed")

Keep side effects outside the sandbox, behind an approval

A sandbox makes computation safe to run; it does not make the result safe to act on. Sending an email, changing a record, deploying code or paying an invoice should happen outside the sandbox, through the same tool gateway and authorization as any other agent action (/domains/ai-emerging/agent-security/tool-authorization/). OWASP's ASI05 guidance says to separate code generation from execution with validation gates and to require human approval for elevated runs, with any allowlist for automatic execution kept under version control.

Show the approver the exact action, arguments and data involved, not the model's summary of them. People approve what an agent recommends more readily than they should, which OWASP lists separately as ASI09 Human-Agent Trust Exploitation; an approval screen that renders the model's explanation instead of the actual diff or payment details invites exactly that.

Computer-use and browser agents: the screen is untrusted input

A computer-use agent reads screenshots and sends clicks and keystrokes, so everything on the screen is input to the model. Anthropic's computer use documentation warns that instructions on web pages or in images might override your instructions, and OpenAI's says to treat screen content as untrusted because text in a page, document or tool result "cannot grant permission or override the user's instructions". Vendors add their own detection: Anthropic runs classifiers over what the tools return, such as screenshots, and when they flag a likely injection they steer the model to check that the instruction really came from the user, and Google offers prompt injection detection for Gemini computer use as an opt-in setting that is off by default. Treat these as a second layer; all three vendors still tell you to isolate the agent.

Their documents converge on the same controls. Run the agent in a dedicated virtual machine, container or browser profile with minimal privileges, start each task from a known clean state, and allowlist the domains it may visit. Keep it away from sensitive data such as account logins: do not leave password managers unlocked or sessions signed in, and hand control to the user when a login is needed, because, as OpenAI's guide puts it, typing sensitive information into a form counts as transmission. Ask a person to confirm anything with real-world consequences: purchases and other financial transactions, sending messages, deleting data, accepting cookies or agreeing to terms. Gemini's API marks such steps with a safety_decision of require_confirmation, and the client must ask the end user and send a safety_acknowledgement before continuing. Finally, set step, time and cost limits, support cancellation, and log screenshots, proposed actions and the actions actually executed.

  • On-screen injection: a page, email or image tells the agent to open another site, download a file or paste data. Allowlist domains and confirm any navigation the task did not call for.
  • Credential entry: the agent types a password or card number into a form it was steered to. Keep credentials out of its reach and let the user sign in.
  • Irreversible clicks: Send, Buy, Delete, Accept. Require confirmation in the client, not a polite instruction in the prompt, and show the person what will happen.

Test escapes, not just features

A sandbox that runs ordinary code proves nothing about hostile code. The agent-code-sandbox project's live tests run both against real Docker: legitimate analysis, a busy loop and a buggy script must still work, while a connection to a canary server on the host, a planted host environment variable, a search for the Docker socket and credential files, a link to a host file left in the output folder, a bounded fork bomb, a 1 GiB allocation, four spinning processes, an endless loop and a 32 MB output flood must all fail or be cut. Each network test has a control run with networking on that does reach the canary, so the test can fail.

Run the same cases against your own runtime after every change to the image, the flags, the container runtime or the host, and record the versions with the results. For a computer-use agent, add pages that carry injected instructions and a fake login form, and check that the agent asks before following them.

Why prompts, denylists and the agent's own process are not sandboxes

"Do not access the network" in the system prompt is a request to the model, not a control; prompt injection exists because the model cannot be relied on to follow one instruction over another. Denylists of imports, functions or shell commands are bypassed by aliases, dynamic imports, encodings and chained commands; Positive Security noted that Auto-GPT's shell command allowlist could be bypassed by chaining a disallowed command after an allowed one.

Running generated code in the agent's own process, with exec or an embedded interpreter, gives it the agent's memory, environment, credentials and network. Running it in a container that mounts the project folder, passes the agent's environment or can reach the Docker socket only moves the problem. The boundary must deny those paths by construction, and the tests must prove it.

Check the result against concrete cases

Each row is generated code or an agent action and the result the boundary must produce. The project on this page runs the first eight against real Docker.

Check the result against concrete cases
CaseExpected resultWhat it establishes
Analysis of an attached CSV that writes out/summary.csvExit 0; JSON on stdout; the CSV returned as dataLegitimate work still succeeds
TCP connection to a canary server on the hostConnection fails; canary receives nothing; a control run with networking on does reach itNo network, proven by a test that can fail
Print an environment variable planted on the hostPrints absentNo secrets pass into the sandbox
A link in the output folder to a host .env fileThe key never comes backOutputs return as data; the host follows no links
Fork bomb bounded at 300 processesFork fails at the process limitProcess limit
Allocate 1 GiBKilled by the OOM killer at 256 MBMemory limit with no swap
Endless loopKilled at the deadline; no container leftThe deadline kills the container, not just the client
Docker unavailableError; nothing runsFails closed, never on the host
Computer-use agent sees "open this link and sign in" on a pageAgent asks the user; no navigation off the allowlistScreen content cannot redirect the task
Agent reaches a Buy or Send buttonConfirmation shows the exact purchase or messageIrreversible actions stay with a person

Guided practice

Your team is adding a "run Python" tool and a browser agent to a support assistant. Review the proposed design, identify how generated code or on-screen content could escape or cause harm, and write the corrected configuration.

Supplied scenario material
Plain text
Code tool (proposal)
  docker run --rm -i -v $PWD/jobs:/work -w /work --env-file .env python:3.14-slim python -c "$CODE"
  Timeout: subprocess.run(..., timeout=60) around the docker command
  If docker is not installed: run the code with the local python3 so the demo works
  Output files: read every file under jobs/<id>/out/ and attach it to the chat

.env (agent's environment)
  ANTHROPIC_API_KEY=...
  SUPPORT_DB_URL=postgres://support:...@db.internal/support

Browser agent (proposal)
  Runs in the support engineer's own Chrome profile, already signed in to the admin console
  May visit any site a ticket links to
  Can click "Issue refund" when the ticket says the customer was double-charged
  1. 1. Find the escapes in the code tool

    List each way generated code could reach secrets, the host or the network.

    Expected result and reasoning

    --env-file .env puts the model API key and the database URL inside the container; the default network lets the code send them out or reach db.internal; the shared jobs folder lets one job read another's files and lets code leave links the host follows when it reads out/; the image tag is not pinned; there are no memory, CPU or process limits; the timeout kills only the docker client; and the local fallback runs the code on the host with everything.

  2. 2. Write the corrected command

    Rewrite the docker run command and the surrounding behavior.

    Expected result and reasoning

    An image pinned by digest; --network=none, --read-only, a size-limited tmpfs, a non-root --user, --cap-drop=ALL, no-new-privileges, --pids-limit, equal --memory and --memory-swap, --cpus and --ulimit=fsize; no -v or --env-file; inputs on stdin and outputs returned as a checked envelope; a deadline that runs docker kill on the container's name; and an error, not a fallback, when Docker is missing.

  3. 3. Fix the browser agent

    Describe where it runs, what it can reach and what needs a person.

    Expected result and reasoning

    A dedicated, disposable VM or browser profile with no saved sessions; an allowlist of domains the support workflow needs; the engineer signs in when needed; ticket text and pages are untrusted; issuing a refund goes through the refund tool with an approval that shows the amount, account and ticket, not through a click the agent makes.

  4. 4. Prove it

    Name four tests you would run before shipping.

    Expected result and reasoning

    A canary connection that fails (with a control that succeeds), a planted host variable that the code cannot print, a link in out/ that does not return the linked file, and an endless loop that leaves no container; for the browser agent, a page with an injected instruction that makes the agent ask the user.

Your deliverable: A list of the escapes you found, the corrected sandbox command and tool behavior, the browser agent's operating rules, and the tests that prove them.

The design is fictional. Run escape tests only against sandboxes you own, with bounded payloads such as the project's, on a host that holds no real credentials.

Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.

No draft saved on this device yet.

Run a local project

Download and unzip a starter with failing tests and a step-by-step README, edit the named exercise file, and run the tests until they pass. The reference solution is a separate download, so you can attempt the repair before you see it.

Python 3.11+ offline · Docker for the live tests · 45–75 min

Run model-generated Python in a locked-down container

Repair an agent's run-Python tool until it refuses to fall back to the host, shares no folder with it, runs each job offline with memory, CPU and process limits, kills the container at the deadline and checks the result envelope the code could have forged.

Needs: The live tests need Docker with Linux containers and internet access once to pull the Python image (about 45 MB compressed). The offline tests need only Python.

In the extracted folder, run:

Shell
python3 -m unittest -v test_sandbox.py
docker pull python:3.14.8-slim-trixie@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170
python3 -m unittest -v test_live.py

13 regression tests · The starter intentionally fails some cases.

Opens in a dev container (VS Code Dev Containers or GitHub Codespaces) with a pinned image.

The offline tests use a stand-in for the docker CLI. The 12 live tests ran in Docker 29.4.3 with python:3.14.8-slim-trixie pinned by digest: network, host environment and files, a link to a host file, a bounded fork bomb, 1 GiB of memory, CPU share, a deadline and an output flood, plus three legitimate jobs. Containers share the host kernel; gVisor and microVMs were not tested.

Sources & further reading

The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.

  1. OWASP Top 10 for Agentic Applications for 2026 (ASI05 Unexpected Code Execution). Version 2026, December 2025.
  2. OWASP AI Agent Security Cheat Sheet
  3. Auto-GPT advisory GHSA-x5gj-2chr-4ch6: Docker escape when running from docker-compose.yml in the git repo (CVE-2023-37273)
  4. Auto-GPT advisory GHSA-5h38-mgp9-rj5f: Python code execution sandbox escape in the non-Docker version (CVE-2023-37274)
  5. Positive Security: Hacking Auto-GPT and escaping its docker container (third-party research). 29 June 2023.
  6. Python documentation: sys.addaudithook (audit hooks are not a sandbox). Python 3.14.
  7. Docker docs: docker container run (option reference)
  8. Docker docs: Resource constraints (memory, swap and CPU)
  9. Docker docs: None network driver
  10. Docker docs: Seccomp security profiles for Docker
  11. Docker docs: Docker Engine security (daemon attack surface)
  12. gVisor: What is gVisor?
  13. gVisor: Docker quick start (--runtime=runsc)
  14. Firecracker microVMs
  15. Wasmtime: Security
  16. Pyodide: Python compatibility (WebAssembly constraints). Pyodide 314.0.7.
  17. Anthropic: Computer use tool (security considerations)
  18. Anthropic: Code execution tool (containers, networking and limits)
  19. OpenAI: Computer use guide (Run safely)
  20. OpenAI: Code Interpreter guide (containers)
  21. Google: Gemini API computer use (safety decisions and best practices)
  22. Google: Gemini API code execution (limitations)

Reference information

ASVS 5.0.0 requirements
v5.0.0-15.2.5 · v5.0.0-15.2.2 · v5.0.0-13.2.4

Download this guide as Markdown