{"course":{"slug":"building-with-ai","title":"Building with AI","url":"https://addaly.com/learn/building-with-ai","level":"intermediate","track":"build"},"lesson":{"slug":"running-code-safely","title":"Letting a model run code without letting it run your machine","url":"https://addaly.com/learn/building-with-ai/running-code-safely","position":29,"of":80,"minutes":9,"answer":"A code tool is a shell rather than a tool, so isolate it in a disposable container with no network and no credentials.","keyIdea":"A code tool is a shell rather than a tool, so isolate it in a disposable container with no network and no credentials, and always show the generated code beside the result because inspectable working is the only real check on it."},"body":{"format":"markdown","text":"## Why a code tool is different from every other tool\n\nEvery other tool has a fixed surface. `order_status` can do exactly one thing, and the worst a wrong call achieves is the wrong order id.\n\nA code-execution tool has no surface. `run_python(code)` accepts arbitrary programs, which means its capability set is whatever the process it runs in can reach: the filesystem, the network, environment variables, other processes, your cloud metadata endpoint. You have not added a tool. You have added a shell.\n\nThat is also why it is valuable. Models are poor at arithmetic and excellent at writing the code that does arithmetic. A model asked to compute a weighted median over 4,000 rows will produce a confident wrong number; asked to write the pandas that computes it, it will usually be right, and the answer is verifiable because you can read the code.\n\nSo it is worth having. It is not worth having on the machine your application runs on.\n\n## The layers, from weakest to strongest\n\n**A restricted interpreter**: stripping builtins, blocking imports, walking the AST. This has been broken repeatedly for decades in every language that has tried it. Python's introspection makes escape routes easy to find and hard to enumerate. Do not rely on it as your only boundary.\n\n**A separate process with resource limits.** `setrlimit` on CPU, memory and file descriptors, a timeout, a low-privilege user. Stops runaway loops. Does not stop reading files or making network calls.\n\n**A container.** A fresh container per execution, no mounted volumes, read-only root filesystem, dropped capabilities, no network, a non-root user, memory and CPU caps, killed after N seconds. This is the practical baseline and it is achievable with Docker or Podman in an afternoon.\n\n**A sandbox with a stronger kernel boundary**: gVisor, Firecracker microVMs, or WebAssembly runtimes such as Pyodide in a worker. Container escapes via kernel bugs are rare but real; these narrow that. WASM is the interesting cheap option: Pyodide runs Python in the browser or in a Wasm runtime with no syscalls at all, which makes the sandbox the default rather than something you configure.\n\n**Someone else's sandbox.** Several providers offer a hosted code interpreter. You have outsourced the hard part, and you have sent them your data.\n\n## The settings that matter most\n\nIf you take four things from this lesson:\n\n**No network by default.** This is the highest-value control by a wide margin. Without egress, code cannot exfiltrate data, cannot fetch a second-stage payload, and cannot reach `169.254.169.254` to steal your cloud instance credentials. Where the task needs a specific host, allowlist that host; never open it generally.\n\n**No credentials in the environment.** Do not pass your API keys, database URLs or cloud role into the sandbox. An empty environment costs nothing and removes the prize.\n\n**Ephemeral and disposable.** A fresh container per run. State that persists between runs is state an earlier run can leave behind for a later one.\n\n**Hard timeout and memory cap.** Enforced by the runtime, not by a `signal` handler inside the code you are trying to constrain.\n\n## Data goes in as data\n\nThe useful pattern is not \"run this code on the server\". It is: copy the specific input into the sandbox, run, copy the output back.\n\n```python\nwith Sandbox(network=False, mem_mb=512, timeout_s=20) as sb:\n    sb.write(\"/work/data.csv\", csv_bytes)      # only what is needed\n    out = sb.run(code, cwd=\"/work\")\n    result = sb.read(\"/work/out.json\") if out.ok else None\n```\n\nCopy in the one file, not the directory. Read back one named artefact, not whatever appeared. Cap the size of both. A model that writes a 4 GB file has otherwise just filled your disk.\n\n## Show the code\n\nA property that makes code execution unusually trustworthy compared with other model outputs: **the working is inspectable**. When an assistant reports a revenue figure, you cannot check its reasoning. When it reports a figure and shows the eleven lines of pandas that produced it, a competent colleague can audit it in thirty seconds.\n\nSo surface the code, always, next to the result. It converts an opaque claim into a reviewable one, and it makes the failure mode legible: a wrong answer usually comes with visibly wrong code, which is the easiest kind of bug to report.\n\nOne caveat to be honest about: a model can write code that runs cleanly, produces a number, and answers a subtly different question than the one asked: filtering on the wrong column, silently dropping nulls, joining on a key with duplicates. The code being visible is what gives you any chance of catching that. It is not a guarantee that anyone will look."},"check":{"question":"A data assistant runs model-written Python in a container with a CPU limit, a memory cap and a 30-second timeout, but with network access left on. What is the largest remaining exposure?","options":[{"text":"Long-running code could still exhaust the host's disk.","correct":false,"tempting":"A real resource concern, but it is a denial-of-service risk rather than the loss of data or credentials."},{"text":"Code can reach the cloud metadata endpoint or an external host, turning any data or credential it can see into an outbound request.","correct":true},{"text":"The model might write code that produces a wrong answer.","correct":false,"tempting":"Always true and mitigated by showing the code, but it is a correctness issue rather than the containment gap the question asks about."},{"text":"Containers share a kernel, so a kernel exploit could escape.","correct":false,"tempting":"A genuine but rare and difficult path, far harder than simply making an HTTP request from code that is already permitted to run."}]},"prev":{"slug":"mcp-and-tool-protocols","title":"MCP: what a tool protocol buys you, and what it costs","url":"https://addaly.com/learn/building-with-ai/mcp-and-tool-protocols"},"next":{"slug":"multi-agent-when-worth-it","title":"Multiple agents, and the honest cost","url":"https://addaly.com/learn/building-with-ai/multi-agent-when-worth-it"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Letting a model run code without letting it run your machine', in Building with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-with-ai/running-code-safely (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Letting a model run code without letting it run your machine","courseTitle":"Building with AI","module":{"index":3,"slug":"structure-tools-and-the-loop","title":"Structure, tools and the loop"},"answer":null,"keyIdea":"A code tool is a shell rather than a tool, so isolate it in a disposable container with no network and no credentials, and always show the generated code beside the result because inspectable working is the only real check on it.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"why-a-code-tool-is-different-from-every-other-tool","runs":[{"text":"Why a code tool is different from every other tool"}]},{"t":"p","runs":[{"text":"Every other tool has a fixed surface. "},{"text":"order_status","code":true},{"text":" can do exactly one thing, and the worst a wrong call achieves is the wrong order id."}]},{"t":"p","runs":[{"text":"A code-execution tool has no surface. "},{"text":"run_python(code)","code":true},{"text":" accepts arbitrary programs, which means its capability set is whatever the process it runs in can reach: the filesystem, the network, environment variables, other processes, your cloud metadata endpoint. You have not added a tool. You have added a shell."}]},{"t":"p","runs":[{"text":"That is also why it is valuable. Models are poor at arithmetic and excellent at writing the code that does arithmetic. A model asked to compute a weighted median over 4,000 rows will produce a confident wrong number; asked to write the pandas that computes it, it will usually be right, and the answer is verifiable because you can read the code."}]},{"t":"p","runs":[{"text":"So it is worth having. It is not worth having on the machine your application runs on."}]},{"t":"h2","id":"the-layers-from-weakest-to-strongest","runs":[{"text":"The layers, from weakest to strongest"}]},{"t":"p","runs":[{"text":"A restricted interpreter","b":true},{"text":": stripping builtins, blocking imports, walking the AST. This has been broken repeatedly for decades in every language that has tried it. Python’s introspection makes escape routes easy to find and hard to enumerate. Do not rely on it as your only boundary."}]},{"t":"p","runs":[{"text":"A separate process with resource limits.","b":true},{"text":" "},{"text":"setrlimit","code":true},{"text":" on CPU, memory and file descriptors, a timeout, a low-privilege user. Stops runaway loops. Does not stop reading files or making network calls."}]},{"t":"p","runs":[{"text":"A container.","b":true},{"text":" A fresh container per execution, no mounted volumes, read-only root filesystem, dropped capabilities, no network, a non-root user, memory and CPU caps, killed after N seconds. This is the practical baseline and it is achievable with Docker or Podman in an afternoon."}]},{"t":"p","runs":[{"text":"A sandbox with a stronger kernel boundary","b":true},{"text":": gVisor, Firecracker microVMs, or WebAssembly runtimes such as Pyodide in a worker. Container escapes via kernel bugs are rare but real; these narrow that. WASM is the interesting cheap option: Pyodide runs Python in the browser or in a Wasm runtime with no syscalls at all, which makes the sandbox the default rather than something you configure."}]},{"t":"p","runs":[{"text":"Someone else’s sandbox.","b":true},{"text":" Several providers offer a hosted code interpreter. You have outsourced the hard part, and you have sent them your data."}]},{"t":"h2","id":"the-settings-that-matter-most","runs":[{"text":"The settings that matter most"}]},{"t":"p","runs":[{"text":"If you take four things from this lesson:"}]},{"t":"p","runs":[{"text":"No network by default.","b":true},{"text":" This is the highest-value control by a wide margin. Without egress, code cannot exfiltrate data, cannot fetch a second-stage payload, and cannot reach "},{"text":"169.254.169.254","code":true},{"text":" to steal your cloud instance credentials. Where the task needs a specific host, allowlist that host; never open it generally."}]},{"t":"p","runs":[{"text":"No credentials in the environment.","b":true},{"text":" Do not pass your API keys, database URLs or cloud role into the sandbox. An empty environment costs nothing and removes the prize."}]},{"t":"p","runs":[{"text":"Ephemeral and disposable.","b":true},{"text":" A fresh container per run. State that persists between runs is state an earlier run can leave behind for a later one."}]},{"t":"p","runs":[{"text":"Hard timeout and memory cap.","b":true},{"text":" Enforced by the runtime, not by a "},{"text":"signal","code":true},{"text":" handler inside the code you are trying to constrain."}]},{"t":"h2","id":"data-goes-in-as-data","runs":[{"text":"Data goes in as data"}]},{"t":"p","runs":[{"text":"The useful pattern is not “run this code on the server”. It is: copy the specific input into the sandbox, run, copy the output back."}]},{"t":"code","lang":"python","label":"Python","text":"with Sandbox(network=False, mem_mb=512, timeout_s=20) as sb:\n    sb.write(\"/work/data.csv\", csv_bytes)      # only what is needed\n    out = sb.run(code, cwd=\"/work\")\n    result = sb.read(\"/work/out.json\") if out.ok else None"},{"t":"p","runs":[{"text":"Copy in the one file, not the directory. Read back one named artefact, not whatever appeared. Cap the size of both. A model that writes a 4 GB file has otherwise just filled your disk."}]},{"t":"h2","id":"show-the-code","runs":[{"text":"Show the code"}]},{"t":"p","runs":[{"text":"A property that makes code execution unusually trustworthy compared with other model outputs: "},{"text":"the working is inspectable","b":true},{"text":". When an assistant reports a revenue figure, you cannot check its reasoning. When it reports a figure and shows the eleven lines of pandas that produced it, a competent colleague can audit it in thirty seconds."}]},{"t":"p","runs":[{"text":"So surface the code, always, next to the result. It converts an opaque claim into a reviewable one, and it makes the failure mode legible: a wrong answer usually comes with visibly wrong code, which is the easiest kind of bug to report."}]},{"t":"p","runs":[{"text":"One caveat to be honest about: a model can write code that runs cleanly, produces a number, and answers a subtly different question than the one asked: filtering on the wrong column, silently dropping nulls, joining on a key with duplicates. The code being visible is what gives you any chance of catching that. It is not a guarantee that anyone will look."}]}],"check":{"question":[{"text":"A data assistant runs model-written Python in a container with a CPU limit, a memory cap and a 30-second timeout, but with network access left on. What is the largest remaining exposure?"}],"options":[{"text":[{"text":"Long-running code could still exhaust the host’s disk."}],"correct":false,"tempting":[{"text":"A real resource concern, but it is a denial-of-service risk rather than the loss of data or credentials."}]},{"text":[{"text":"Code can reach the cloud metadata endpoint or an external host, turning any data or credential it can see into an outbound request."}],"correct":true,"tempting":null},{"text":[{"text":"The model might write code that produces a wrong answer."}],"correct":false,"tempting":[{"text":"Always true and mitigated by showing the code, but it is a correctness issue rather than the containment gap the question asks about."}]},{"text":[{"text":"Containers share a kernel, so a kernel exploit could escape."}],"correct":false,"tempting":[{"text":"A genuine but rare and difficult path, far harder than simply making an HTTP request from code that is already permitted to run."}]}],"order":[2,0,3,1],"why":null},"prev":{"slug":"mcp-and-tool-protocols","title":"MCP: what a tool protocol buys you, and what it costs","minutes":9},"next":{"slug":"multi-agent-when-worth-it","title":"Multiple agents, and the honest cost","minutes":9}}}