Every previous chapter treated tool calls as a bounded set: send this email, refund that order, search these documents. A finite menu, each item with a policy.
Code execution breaks that model. A tool that runs code the model wrote has one entry on the menu and an unbounded range of effects. There is no schema for arbitrary program, and no policy engine that can decide, by reading a C# file, whether running it is a good idea.
So the containment moves. Instead of deciding whether to run the code, you decide what the code is able to reach.
Coding agents dominate the agentic incident record. Of the 53 agentic projects tracked by OWASP's surveyor, 28 are coding agents.
Reporting from June 2026 put the advisory counts at n8n 57, Claude Code 22, AutoGPT 15, Dify 13 and Roo-Code 11. Those are countable from GitHub's public advisory API, so this book counted them again on 14 September 2026.
| Project | Reported, Jun 2026 | Counted, 14 Sep 2026 |
|---|---|---|
| n8n | 57 | 180 |
| AutoGPT | 15 | 40 |
| Claude Code | 22 | 30 |
| Dify | 13 | 21 |
| Roo-Code | 11 | 11 |
Three months, and n8n more than tripled. AutoGPT and Claude Code swapped places, so even the ordering in the original reporting no longer holds.
Now look at when n8n's 180 advisories were published: 14 in 2025, and 166 in 2026.
No project becomes an order of magnitude less secure in a year. What happened is that n8n started running a disclosure programme, and researchers started looking. That is the caveat, demonstrated rather than asserted: advisory counts track disclosure activity and project popularity, not relative insecurity. A high count is frequently a sign of a project taking security seriously, and the projects you should actually worry about are the ones reporting nothing.
Say that out loud every time you use these figures, including the ones in the table above. What the distribution supports is only the weaker and more useful claim: this is where the attention is, and it is where the attack surface has grown fastest.
The Replit incident from chapter 1 is the canonical shape. An agent with shell access, a standing production credential, and an instruction it was free to disregard. Nobody needed to inject anything.
Start from chapter 6, because it settles the question people argue about.
An agent that has read any untrusted content produces tainted output. Code is output. Therefore code produced by an agent that read a customer's uploaded file is tainted code, and executing tainted code is the most consequential action in this book.
That sounds like it should end the chapter with "so do not do it." It does not, because the feature is often worth having and the alternative most teams choose is worse: running it anyway and hoping.
The workable position is that tainted code may execute inside an environment where execution does not matter much. Everything in this chapter is about constructing that environment.
A sandbox worth the name has four, and they are not negotiable individually.
No credentials. Nothing in the environment, the process, the filesystem or the metadata service. No cloud instance identity, no mounted secrets, no .env, no inherited environment variables. This is the property most often violated, usually by an agent inheriting the parent process environment because that was the default.
No network, or a proxy that applies chapter 10. Default deny outbound. If the code needs a package registry, that is one allowlisted host through a proxy that logs, and not general internet access.
A filesystem scoped to the task. A fresh writable directory containing only the inputs, and nothing mounted from the host that was not deliberately placed there.
Ephemeral. The environment is created for the task and destroyed after it. State that survives is state an attacker can use next time, which is chapter 12's subject arriving early.
public sealed record SandboxSpec(
string Image,
TimeSpan Timeout,
long MemoryBytes,
IReadOnlyList<string> AllowedHosts, // empty = no network
IReadOnlyDictionary<string, string> Inputs);Note what is missing from that record. There is no field for credentials, no field for mounts, and no field for environment variables. Types are a good place to make the wrong thing unrepresentable, and a sandbox spec that cannot express "give it my AWS key" is a sandbox spec nobody can accidentally misuse.
The four properties above are enforced by the operating system, a container runtime, or a microVM. They are not enforced by your C#.
This matters because the tempting implementation is a permission check inside the execution tool: inspect the code, look for dangerous calls, refuse the bad ones. That is chapter 4's argument again in a new costume. Static analysis of adversarial code is a classifier over an unbounded input space, and the attacker writes the input.
Use a real isolation boundary. Containers with a restrictive profile are the common answer and are adequate for most threat models. A microVM is better and costs more. Running the code in the agent's own process is not a sandbox, whatever the wrapper is called.
public async Task<SandboxResult> RunAsync(
Tagged<string> code, SandboxSpec spec, CancellationToken ct)
{
await using var box = await _runtime.CreateAsync(spec, ct);
try
{
return await box.ExecuteAsync(code.Value, spec.Timeout, ct);
}
catch (OperationCanceledException)
{
return SandboxResult.TimedOut();
}
finally
{
await box.DestroyAsync(); // runs on every path
}
}The finally is the part to get right. A sandbox that leaks on the exception path is a sandbox that an attacker will learn to make throw.
The output of executing tainted code is tainted, and this trips teams who have done everything else correctly.
The sandbox prints a result. That result goes back into the agent's context, where it is read by a model that then decides what to do next. If the executed code was written by an attacker, so was its output, and the attacker now has a direct write into your planner's context window.
var result = await _sandbox.RunAsync(code, spec, ct);
return Tagged<string>.Taint(result.Stdout);Chapter 7's boundary applies here with full force. Where practical, the planner should receive a typed extraction of the result through the quarantine rather than raw stdout. Exit code and a schema-constrained summary, not eighty lines of program output.
Download the full PDF for free?
Free download — no account required