Sandbox your Mastra agent with IronClaw¶
You built an agent with Mastra, the TypeScript agent framework: a model, an
Agent, and tools created with createTool. That is a great way to design
behavior. The problem starts when it runs somewhere real: a Mastra tool's
execute runs in your own Node process, with your API key in memory and
unrestricted outbound network. One prompt-injected instruction and a shell tool is a
shell on your box.
IronClaw runs the same job behind a sealed sandbox instead: no network card, the model key held host-side and never in the agent, and every privileged tool call routed through a human-approval gateway and an audit log.
Runnable example
A one-command Mastra-to-IronClaw example lives at
examples/integrations/mastra:
a real Mastra tool (createTool) whose execute runs inside an IronClaw
sandbox, driven over one benign task plus a battery of escape attempts, each
reported blocked. Zero credentials, just Docker and Node.js 22+.
Why sandbox this¶
A typical Mastra tool executes on the host:
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import { execSync } from "node:child_process";
export const shellTool = createTool({
id: "shell",
description: "Run a shell command",
inputSchema: z.object({ command: z.string() }),
execute: async ({ command }) => execSync(command).toString(), // runs on YOUR box
});
Three things are true of that tool, and all three are risks:
- The key is in the process. Anything that can read
process.envcan read your provider key. executeruns with your privileges.execSyncruns on your host, with your filesystem and network.- Egress is wide open. The process can reach any host on the internet.
IronClaw closes all three by construction. The example keeps your agent exactly as designed and points the tool's execution at a sealed per-session sandbox.
See it work first (no credentials)¶
git clone https://github.com/IronSecCo/ironclaw.git && cd ironclaw
examples/integrations/mastra/run.sh
It brings up the offline mock provider (no model key), builds a real Mastra tool,
and drives its execute over one benign task plus a battery of escape attempts —
network exfil, host-filesystem read, Docker-socket takeover — each reported
BLOCKED. The run exits non-zero if any containment expectation fails, so it
doubles as a CI check.
The fix, in your agent¶
Keep the agent; swap the tool. The example's makeSandboxTool(sandbox) returns an
ordinary Mastra tool, so it drops into new Agent({ tools: { ... } }) with no other
change:
import { IronClawSandbox } from "../_shared/ironclaw-sandbox";
import { makeSandboxTool } from "./ironclaw-tool";
const sandbox = await new IronClawSandbox().engage();
const tool = makeSandboxTool(sandbox); // execute() runs inside the sealed sandbox
// const agent = new Agent({ tools: { sandboxedShell: tool }, ... });
Every command the model routes through that tool now executes inside the box as an
unprivileged uid, with network=none and no host mounts.
What you gained¶
- The key left the agent. It lives host-side and is injected per request; a compromised agent has nothing to steal.
network=noneby default. The sandbox has no NIC. The only egress is the audited model-proxy socket, plus whatever hosts you explicitly allowlist.- Privileged actions are gated. Registering a tool, spawning another agent, or reaching a new host flows through a human-approval gateway and the audit log.
Same agent you designed in Mastra. A perimeter it never had.