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.

i
This is safe to leave unattended because Hiver, not the agent, enforces the boundary. Each sandbox runs in its own microVM or container with its own network namespace, isolated from the host and every other sandbox at the kernel level, and it only ever sees its own filesystem. Set an egress policy and that allow-list is enforced outside the guest, so even with 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.
agent/index.ts
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"));
agent/main.py
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:

agent/package.json
{
  "name": "claude-agent-sdk-example",
  "scripts": {
    "start": "tsx index.ts"
  },
  "dependencies": {
    "@anthropic-ai/claude-agent-sdk": "latest",
    "express": "latest"
  },
  "devDependencies": {
    "tsx": "latest"
  }
}
agent/Dockerfile
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:

agent/requirements.txt
claude-agent-sdk
flask[async]
agent/Dockerfile
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-agent

Now 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:

index.ts
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:

index.ts
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).

main.py
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"])
i
Whichever shape you pick, your provider key stays where the loop runs. Run the loop outside and the key never enters the sandbox; run it inside and it lives only in the sandbox env (or inject it with an egress override). The sandbox reaches only the hosts your egress policy allows.

Next: OpenAI Agents SDK