The sandbox
/workspace, a fixed toolchain, and only the network you bind.A sandbox is the machine one task runs in. It is torn down after 10 minutes idle by default; the next message starts a fresh one with the full history.
You need: a recipe, and introspection login to bind hosts and variables.
What it can reach
Only bound hosts, over HTTPS. Anything else fails with
403 Destination host not on session allowlist. Allowed by default:
github.com and api.github.com (granted repositories),
registry.npmjs.org, pypi.org, files.pythonhosted.org, slack.com,
files.slack.com, mcp.deepwiki.com, mcp.linear.app, and the managed model
providers.
To allow another host, create an api endpoint with no header:
introspection bindings endpoints create --kind api --name sec \
--endpoint-url https://www.sec.gov -r support-agentAn endpoint names one exact host. Hosts reached only through URLs that carry
their own credential (pre-signed links, signed WebSocket URLs) go in the
endpoint’s metadata.extra_hosts, which accepts *.example.com. No raw TCP:
put an HTTPS API in front of a database.
What’s installed
Node 24 (npm, pnpm, tsx), Python 3.14 with uv, git, gh, curl,
jq, openssl, ripgrep, fd, unzip, and Poppler (pdftotext,
pdftoppm). No other language runtimes, browser, ffmpeg, or apt. Declare
dependencies in package.json.
Built-in tools
read (images go to the model as images), bash, edit, write, grep,
find, ls. An agent gets only what its tools: list names.
Block a tool call
A tool_call hook sees every call before it runs. Return
{ block: true, reason } to refuse; the agent sees the reason.
This one caps refunds at an amount set per environment; larger ones become drafts:
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
const extension: ExtensionFactory = (pi) => {
pi.on("tool_call", async (event) => {
if (event.toolName !== "issue_refund") return;
const limit = Number(process.env.REFUND_LIMIT);
const amount = Number((event.input as { amount?: unknown }).amount);
if (!Number.isFinite(limit) || !(amount <= limit)) {
return {
block: true,
reason: `Refunds over ${process.env.REFUND_LIMIT ?? "the limit"} need approval. Call save_draft instead.`,
};
}
});
};
export default extension;Set the limit as a binding variable; the sandbox exposes it in process.env:
export REFUND_LIMIT=500
introspection bindings variables create --name REFUND_LIMIT --from-env REFUND_LIMIT \
-r support-agent -e productionDeclare the file under pi.extensions (Add a tool). A missing
variable blocks every refund, so a forgotten binding fails closed.
- Throwing also blocks. Mutate
event.inputto rewrite arguments. - MCP tools in
toolsmode are namedmcp_<server>_<tool>_<hash>: match the prefix. Inclimode they arebashcommands. - For no network shell at all, leave
bashout and usemcp.mode: tools.
Secrets
A credential reaches an API only as a header the egress adds for that host (Connect services and secrets). It never enters the sandbox. Variables are readable in the sandbox: never store a secret in one.
Verify
With issue_refund and save_draft tools in the recipe:
REFUND_LIMIT=500 introspection local -p "Refund order 1842 for 900 dollars."Expect a blocked issue_refund call, then a save_draft call.
Limits
| Not supported | Do this instead |
|---|---|
| Asking for approval inside a hook | The sandbox is headless. Draft into your system (Approve outside the chat) or ask the user. |
Private networks, IPs, localhost, custom ports | Expose a public HTTPS endpoint, or run Introspection in your cloud (Security). |
| Mid-turn checkpoints | A sandbox lost mid-turn fails the task until the next message restarts it. Keep results in the /workspace folders that survive (Files and memory). |
Or ask your agent: “Block refunds over the REFUND_LIMIT variable and make the agent draft them instead.”