Claude Agent SDK
The Claude Agent SDK bundles a native Claude Code binary and drives it as a subprocess, giving your agent built-in Bash, Read, Write, Edit, Glob, and Grep tools. Claude's sandbox-environments guide frames the choice as where the whole agent process runs: keep it on your host and only its Bash commands are sandboxed, or run it inside a container or VM that becomes the isolation boundary for every tool, MCP server, and hook. A Hiver sandbox is that container/VM boundary. Running the loop inside it is the preferred shape; keeping the loop outside is a fallback for when the credential must stay off the sandbox.
Run the loop inside the sandbox
This is the preferred shape. Ship the agent as the sandbox's own service: wrap query() in an HTTP server that runs inside the sandbox. Because the whole process lives there, every built-in tool resolves against the sandbox filesystem, Read, Write, Edit, Glob, and Grep as well as Bash, so they all see one consistent view and there is nothing to register. The sandbox is also the isolation boundary the guide calls for, which is what makes permissionMode: "bypassPermissions" safe here: the whole agent, MCP servers and hooks included, is already confined to the sandbox, so you can run it unattended the way the guide reserves for a container or VM but never for your host.
bypassPermissions a prompt-injected agent reaches only the hosts you permit, everything else is denied. That infrastructure boundary, not the model's cooperation, is what bypassPermissions relies on.import express from "express";
import { query } from "@anthropic-ai/claude-agent-sdk";
const app = express();
app.use(express.json());
app.post("/chat", async (req, res) => {
let reply = "";
for await (const msg of query({
prompt: req.body.prompt,
options: {
model: "claude-opus-4-8",
cwd: "/workspace",
allowedTools: [
"Bash",
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"WebSearch",
],
permissionMode: "bypassPermissions",
},
})) {
if (msg.type === "result" && msg.subtype === "success") {
reply = msg.result;
}
}
res.json({ reply });
});
app.listen(3000, () => console.log("listening on :3000"));from flask import Flask, request, jsonify
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
app = Flask(__name__)
@app.post("/chat")
async def chat():
reply = ""
async for message in query(
prompt=request.json["prompt"],
options=ClaudeAgentOptions(
model="claude-opus-4-8",
cwd="/workspace",
allowed_tools=[
"Bash",
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"WebSearch",
],
permission_mode="bypassPermissions",
),
):
if (
isinstance(message, ResultMessage)
and message.subtype == "success"
):
reply = message.result
return jsonify(reply=reply)
app.run(host="0.0.0.0", port=3000)Package the server as an image Hiver can run. Alongside the server file, declare its dependencies and a Dockerfile that installs them, EXPOSEs port 3000, and starts the server:
{
"name": "claude-agent-sdk-example",
"scripts": {
"start": "tsx index.ts"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "latest",
"express": "latest"
},
"devDependencies": {
"tsx": "latest"
}
}FROM node:22-slim
ENV IS_SANDBOX=1
WORKDIR /app
COPY package.json .
RUN npm install
COPY index.ts .
EXPOSE 3000
CMD ["npx", "tsx", "index.ts"]For Python, install claude-agent-sdk instead and swap the base image:
claude-agent-sdk
flask[async]FROM python:3.13-slim
ENV IS_SANDBOX=1
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY main.py .
EXPOSE 3000
CMD ["python", "main.py"]Bundle the agent/ directory into a tagged image with the CLI (see OCI Image for more):
hiver bundle ./agent --tag my-agentNow start a sandbox from that image and send it a prompt. getOrCreateSandbox boots the sandbox, proxyUrl(3000) builds a URL that routes to the server inside it:
import { getOrCreateSandbox } from "@hiver.sh/client";
const sandbox = await getOrCreateSandbox("my-agent", {
image: "my-agent",
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
const res = await fetch(`${sandbox.proxyUrl(3000)}chat`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ prompt: "Create /workspace/fib.py and run it." }),
});
console.log((await res.json()).reply);Run the loop outside the sandbox
Keep the loop, and your ANTHROPIC_API_KEY, on your host and register a tool whose handler calls sandbox.exec. This is the security-boundary pattern from the guide: the credential stays outside the sandbox while everything the model runs lands inside it. The tradeoff is that only what you route through this tool reaches the sandbox, the SDK's built-in Read, Write, Edit, Glob, and Grep still run on your host, so expose only the sandbox bash tool (as allowedTools does below) and have the model read and write through it. The SDK registers tools through an in-process MCP server:
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { getOrCreateSandbox } from "@hiver.sh/client";
import { z } from "zod";
const sandbox = await getOrCreateSandbox("agent-tools", { image: "python" });
const tools = createSdkMcpServer({
name: "sandbox",
tools: [
tool("bash", "Run a shell command in the sandbox.", { command: z.string() }, async ({ command }) => {
const { stdout, stderr } = await sandbox.exec(["bash", "-lc", command]);
return { content: [{ type: "text", text: stdout || stderr }] };
}),
],
});
for await (const message of query({
prompt: "List the Python files under /workspace.",
options: { mcpServers: { sandbox: tools }, allowedTools: ["mcp__sandbox__bash"] },
})) {
if (message.type === "result" && message.subtype === "success") console.log(message.result);
}Drive it from another language
The in-sandbox server is just an HTTP service, so you can start and call it from any Hiver client. Provision the my-agent sandbox and POST to proxyUrl(3000)/chat (proxy_url / ProxyURL).
import os, httpx
from hiver import get_or_create_sandbox, SandboxConfig
sandbox = await get_or_create_sandbox("my-agent", SandboxConfig(
image="my-agent",
env={"ANTHROPIC_API_KEY": os.environ["ANTHROPIC_API_KEY"]},
))
async with httpx.AsyncClient() as http:
res = await http.post(
f"{sandbox.proxy_url(3000)}chat",
json={"prompt": "Create /workspace/fib.py and run it."},
)
print(res.json()["reply"])env (or inject it with an egress override). The sandbox reaches only the hosts your egress policy allows.Next: OpenAI Agents SDK