Skip to content

2.2 Sandboxed tools

Back in 0.2 we split tools into safe (read the world) and dangerous (change it). A demo agent runs both without a second thought. That’s failure mode #2 from 1.2: a tool can do anything.

The scary case is the most powerful tool of all: running code the model wrote. If the model can ask to execute arbitrary code and something just… runs it, then a wrong guess isn’t a bad answer — it’s rm -rf, a leaked secret, or a process that never returns.

Not the tool’s. A tool is just a function; it does what it’s told. If safety lived inside each tool, you’d have to make every tool paranoid, forever.

Instead, safety is the harness’s job. The harness sits between the model and every tool and gets to say “no” — or “yes, but in a box.” Two mechanisms:

  • A policy — a gate that inspects every tool call before it runs and decides: allow, sandbox, or block.
  • A sandbox — an isolated runtime with strict limits (time, memory, no network, no filesystem) where untrusted code can run without being able to hurt anything.

Below, the model fires off five tool requests — some innocent, some not. Every one passes through the policy gate first. Step through and watch what the gate does with each.

Policy rulebooksafe readsALLOWmodel-written codeSANDBOXdestructive writesBLOCK
model requests
searchDocs("Q3 invoices")
POLICY GATE
ALLOW
outcome
returns 12 matching documents

A safe read. It only looks at the world, so the gate lets it straight through.

Request 1 of 5

Three behaviors to notice:

  1. Safe reads pass untouched — no ceremony for a document search.
  2. Model-written code is sandboxed. The infinite loop doesn’t hang the harness; the sandbox kills it at the time limit. Harmless code in the same sandbox runs fine — the box is a boundary, not a ban.
  3. Destructive writes are blocked and return a structured error instead of running. The model reads that error and self-corrects to a safe action.

One choke point that every tool call flows through:

async function runToolCall(call) {
const decision = policy.check(call); // inspect BEFORE running
if (decision === 'block') {
// Hand back a structured error — the model can read it and try something else.
return { error: 'blocked', reason: 'destructive action requires approval' };
}
if (decision === 'sandbox') {
// Untrusted code runs isolated, with a hard timeout and no ambient access.
return runInSandbox(call, { timeoutMs: 2000, network: false, fs: false });
}
return tools[call.name](call.args); // safe: run normally
}

The key line is the first one: nothing runs until the policy has seen it. That single choke point is what turns “a tool can do anything” into “a tool can do exactly what the harness permits.”

Nobody hand-rolls a secure sandbox — getting isolation right is genuinely hard. The real tools:

  • e2b, Firecracker microVMs, and hosted code-execution sandboxes for running untrusted/model-written code.
  • Policy layers and permission systems in agent frameworks for the allow/block gate.

You built the toy version — a gate plus an isolated runner — so you can recognize what those products are doing and why they exist.

© 2026 Clifford Bernard · Content CC BY 4.0 · Code MIT ·Source