Explainer

Guarding code-mode and sandboxed agents

In code mode, the model writes a program that calls tools, and a sandbox runs it. One run can make dozens of tool calls that no person ever reads. The sandbox’s tool proxy is the one place every call passes through, which makes it the right place for a guard.

5 min readLast updated

Checks run on our GPUs in Switzerland. Request payloads are not stored.Security

In 30 seconds

  • Code mode lets a model write code against a tool API instead of calling tools one at a time. It is efficient, and it hides individual calls from view.
  • The sandbox isolates the code, but the tool calls it makes still reach real systems through a proxy.
  • Put the guard in that proxy: every call is checked, whatever code produced it.
  • Use batch checks and fail closed for risky tools, so guarding stays fast and safe.

Code and dense tables are folded away. Open any of them on demand.

What code mode is

With classic tool calling, the model emits one tool call, the client runs it, the result goes back into the context, and the model decides the next step. Each call is visible in the transcript.

Code mode changes this. The tools are presented to the model as a programming interface (for example a TypeScript API generated from MCP tool schemas), and the model writes a short program that calls them. A sandbox runs the program and returns only the final output. Cloudflare described this approach as "Code Mode" in 2025, and Anthropic's engineering post "Code execution with MCP" describes the same pattern. The motivations are practical:

  • models are good at writing code, including loops, filters and error handling;
  • intermediate results stay in the sandbox instead of filling the context window;
  • many tool calls can happen in one step, which saves time and tokens.

The trade-off is visibility. A program that loops over 200 customer records and calls crm.update on each one produces 200 side effects from a single model turn. Nobody reviews them one by one.

The sandbox contains code, not consequences

A sandbox (a V8 isolate, a container, a microVM) is good at one thing: stopping the generated code from touching the host. It limits file system access, network access and resource use.

It does not make the tool calls safe. By design, the code is allowed to call tools, and those tools act on real systems with real credentials: they write to the database, send email, open pull requests. A perfectly isolated sandbox can still run db.execute("DELETE FROM invoices") if the tool proxy passes it through.

So the security question for code mode is not "can the code escape?" (the sandbox's job) but "should each tool call the code makes be allowed to happen?" (a policy question).

Put the guard in the tool proxy

In a well-built code-mode setup, generated code cannot reach tools directly. It calls stubs, and each stub sends the request through a tool proxy outside the sandbox, which holds the credentials and forwards the call. Every call passes that point, whatever the code looks like, however it was obfuscated.

That proxy is where the guard belongs:

Show technical details· ts sample
ts
// Inside the tool proxy, outside the sandbox.
async function dispatch(call: { tool: string; args: unknown }, task: Task) {
  const res = await fetch('https://api.mcp-guard.ai/v1/guard', {
    method: 'POST',
    headers: { Authorization: `Bearer ${env.MCP_GUARD_API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      action: call,
      intent: task.userRequest,
      trigger: task.hasReadExternalContent ? 'tool_result' : 'user_request',
      constraints: task.constraints,
      context: 'code-mode run; environment: ' + task.environment,
    }),
    signal: AbortSignal.timeout(2000),
  }).catch(() => null);

  const verdict = res && res.ok ? (await res.json()).verdict : 'ask'; // fail closed
  if (verdict === 'allow') return tools[call.tool](call.args);
  if (verdict === 'ask') return queueForApproval(call, task);
  throw new ToolBlocked(call.tool); // surfaces to the generated code as an error
}

A few design points:

  • Check in the proxy, not in the generated code. Code the model writes can skip a check; the proxy cannot be skipped.
  • Pass the user's request, not the code. The guard judges the action against the intent. The program text is rarely needed.
  • Tell the proxy what the code has read. If the run fetched external content earlier, set trigger to tool_result for later calls, since those calls may be influenced by it.
  • Return blocks as errors. Generated code can catch a ToolBlocked error and report back, which gives the model a chance to recover.

Keeping it fast: batches and budgets

Code mode is attractive because it is fast, so the guard must not become the bottleneck. In our measurements a full check takes about 15 ms on an RTX 4090 (p95 about 23 ms, batch 1), and one GPU sustains roughly 400 checks per second. Over the internet, add the network round trip (about 100 ms is typical from Europe).

Two techniques keep the overhead small:

  • Batch where the code allows it. When generated code maps a tool over a list, the proxy can collect the calls and send them in one POST /v1/guard/batch request (up to 64 checks), then execute only the allowed ones. One round trip instead of many.
  • Skip read-only tools deliberately. If a tool cannot change anything (a pure lookup on data the agent may already see), you may choose not to guard it. Make that an explicit allowlist in the proxy, not a default. Note that reads can still leak data if a later call sends it out; the exfiltration score applies to the call that sends.

At $0.20 per 1,000 checks, checking every mutating call in a run costs little compared with the model call that produced the code.

Fail closed, and keep the other layers

If the guard cannot be reached (a timeout, a 503), the proxy has to decide. For tools that can change or send data, fail closed: treat the call as ask or block. The MCP Guard API returns 503 for retryable failures and charges nothing for them, so a retry with a short backoff is also reasonable.

And keep the controls that do not depend on any model:

  • Scoped credentials in the proxy. The sandbox never sees them; the proxy holds the least privilege the task needs.
  • Hard rules first. A denylist in the proxy for operations that should never run from generated code, checked before the guard.
  • Rate and volume limits. A cap on how many mutating calls one run may make stops a runaway loop even when each call looks fine on its own.
  • Audit logs on your side. Record which calls ran and their verdicts in your own systems; MCP Guard does not store request payloads.

The guard reduces the chance that a bad call gets through. These layers limit the damage when one does.

Frequently asked questions

Does the guard need to see the generated code?
Usually not. It judges each tool call against the user’s intent and constraints. You can include a short description of what the run is doing in the context field if it helps.
Should I guard read-only tool calls?
Reads are low risk on their own, and many teams skip them for speed. The risk comes when data read in one call is sent out in another, so always guard calls that send, write or delete. If you skip reads, do it with an explicit allowlist.
What happens to a blocked call inside running code?
That is up to your proxy. Throwing an error that the generated code can catch works well: the program can stop or report, and the model sees why the call was refused.

Sources

Last updated . MCP Guard reduces the risk of harmful agent actions; it does not guarantee safety. Keep deterministic controls in place alongside it. Third-party names are used only to describe their products; we are not affiliated with them.