---
title: "Host a Claude Agent SDK app in a sandbox VM"
description: "Run a Claude Agent SDK app on an exe.dev VM: Claude Code's agent loop in a sandbox of its own, a private web chat, and Claude with no API key."
url: https://exe-staging.dev/deploy/claude-agent-sdk
category: "AI agents"
updated: 2026-10-20
---

# Host a Claude Agent SDK app in a sandbox VM

Claude Code as a library, with a whole computer to run on.

- **Software:** Claude Agent SDK (https://code.claude.com/docs/en/agent-sdk/overview)
- **Source:** https://github.com/anthropics/claude-agent-sdk-typescript
- **License:** Proprietary (Anthropic Commercial Terms)
- **Time:** 5 minutes
- **Example VM name:** `my-claude-agent-sdk` (yours will differ; on the VM, `hostname` prints it)
- **Machine-readable:** https://exe-staging.dev/deploy/claude-agent-sdk/manifest.json

## What you get

- A Claude Agent SDK app under systemd, with a private web chat at `https://<vm>.exe.xyz/` that shows each command Claude runs.
- Claude working without permission prompts, as a non-root user with sudo, on a VM that holds nothing else.
- Claude Sonnet through exe.dev's LLM integration. No Anthropic API key on the VM.
- Conversations that resume after a restart, and turns that finish after you close the tab.

Running on the VM:

- **Your Claude agent**: Claude Agent SDK 0.3 on Node.js 24, managed by systemd, port 8000, data in `~/claude-agent`
- **Workspace**: Where Claude reads, writes, and runs commands, data in `~/claude-agent/workspace`
- **Conversations**: Claude Code transcripts, resumed after restarts, data in `~/.claude/projects`
- **Claude Sonnet**: Anthropic, through exe.dev's LLM integration

How it is reached:

- https://my-claude-agent-sdk.exe.xyz/ (private): The chat, for you and anyone you share the VM with
- https://my-claude-agent-sdk.exe.xyz:3000/ (private): Apps Claude builds, on ports 3000-9999

## Deploy with Shelley

Open https://exe-staging.dev/new?prompt=Set%20up%20Claude%20Agent%20SDK%20on%20this%20VM%20by%20following%20the%20exe.dev%20recipe%20at%20https%3A%2F%2Fexe-staging.dev%2Fdeploy%2Fclaude-agent-sdk.md%20from%20top%20to%20bottom.%0A%0AThe%20recipe%27s%20example%20VM%20name%20is%20my-claude-agent-sdk.%20This%20VM%27s%20name%20is%20the%20output%20of%20%60hostname%60%3A%20use%20it%20wherever%20the%20recipe%20writes%20my-claude-agent-sdk.%20Skip%20the%20step%20that%20creates%20the%20VM%3B%20you%20are%20on%20it.%0A%0AYou%20are%20done%20when%20all%20of%20these%20are%20true.%20Check%20each%20one%20yourself%3A%0A-%20A%20Claude%20Agent%20SDK%20app%20under%20systemd%2C%20with%20a%20private%20web%20chat%20at%20%60https%3A%2F%2F%3Cvm%3E.exe.xyz%2F%60%20that%20shows%20each%20command%20Claude%20runs.%0A-%20Claude%20working%20without%20permission%20prompts%2C%20as%20a%20non-root%20user%20with%20sudo%2C%20on%20a%20VM%20that%20holds%20nothing%20else.%0A-%20Claude%20Sonnet%20through%20exe.dev%27s%20LLM%20integration.%20No%20Anthropic%20API%20key%20on%20the%20VM.%0A-%20Conversations%20that%20resume%20after%20a%20restart%2C%20and%20turns%20that%20finish%20after%20you%20close%20the%20tab.%0A%0ARead%20the%20whole%20recipe%2C%20including%20the%20closed%20details.%20Follow%20%22Install%20by%0Ahand%22%20first%2C%20skipping%20VM%20creation%3B%20the%20walkthrough%20is%20for%20the%20owner%0Aafterwards.%20Do%20not%20run%20the%20Update%20instructions%2C%20add%20GitHub%20integrations%2C%0Aor%20create%20sandbox%20VMs.%20Keep%20the%20VM%20private%3A%20never%20run%20share%20set-public%2C%0Abecause%20Claude%20runs%20commands%20with%20sudo%20and%20without%20asking.%20On%20an%0Aexisting%20VM%2C%20ask%20before%20replacing%20~%2Fclaude-agent%20or%20anything%20that%0Alistens%20on%20port%208000.%0AVerify%20the%20pong%20reply%2C%20that%20the%20follow-up%20after%20the%20restart%20answers%0Akiwi%2C%20and%20that%20claude-agent.service%20is%20enabled.%0AFinish%20with%20https%3A%2F%2F%3Cvm%3E.exe.xyz%2F%2C%20using%20this%20VM%27s%20name%2C%20and%20suggest%20the%0Afirst%20prompt%20in%20%22Say%20hello%22.%0A%0AFinish%20with%20the%20URLs%20I%20should%20open.%0A to create a VM whose agent, Shelley, follows this recipe. From a terminal:

```sh
curl -fsSL https://exe-staging.dev/deploy/claude-agent-sdk/prompt.txt | ssh exe.dev new --prompt=/dev/stdin
```

Or follow the steps below by hand.

The [Claude Agent SDK](https://code.claude.com/docs/en/agent-sdk/overview)
is Claude Code as a library: the same agent loop, tools, and context
handling, called from your own TypeScript or Python. Every conversation
runs a Claude Code process that reads files, edits them, and runs shell
commands, so an SDK app needs a computer where that's safe. Anthropic's
[hosting guide](https://code.claude.com/docs/en/agent-sdk/hosting) says to
run it in a sandbox, with a disk for its state and API keys kept out of
reach.

An exe.dev VM is that sandbox, and it stays put: a whole Ubuntu machine
with sudo, a disk for files and conversations, a private HTTPS address,
and Claude models through exe.dev's
[LLM integration](/docs/integrations-llm), so no Anthropic API key sits on
the VM. This recipe is a small Claude agent on the SDK: a web chat where
Claude works on the VM without asking permission first.

## Say hello

Open `https://my-claude-agent-sdk.exe.xyz/` and sign in to exe.dev. Try:

> Look around this VM and describe it in three short lines.

Each command Claude runs shows up in grey above its reply. Then:

> Make a one-page to-do list web app and serve it on port 3000. Give me the link.

Claude writes it, starts it as a systemd service, and links
`https://my-claude-agent-sdk.exe.xyz:3000/`, which is private to you like
the chat. Ports 3000 to 9999 all work this way through the
[HTTPS proxy](/docs/proxy).

> [!WARNING]
> **Keep this VM private.** Claude runs commands with sudo, without
> asking, and anyone who can open the page can tell it what to do.

## Walk away

Ask for something that takes a while:

> Install PostgreSQL, load a million rows of made-up orders, and find a slow query you can make fast with an index.

Close the tab. Claude keeps working on the VM, and when you come back the
page shows how far it got. Conversations are files on disk, so they carry
on after a restart or a reboot, too; **New chat** starts a fresh one.

## Hand it a repo

Link GitHub on the [Integrations page](https://exe.dev/integrations), then
give this VM one repository. On your computer:

```bash
ssh exe.dev integrations add github --name myapp --repository you/myapp --attach vm:my-claude-agent-sdk
```

Then ask:

> Clone https://github.int.exe.xyz/you/myapp.git, fix issue 12, push the fix to a new branch, and open a pull request.

No GitHub token reaches the VM: the
[GitHub integration](/docs/integrations-github) adds credentials on the
way out, for `git` and for `gh`. Commits show up as
`exe-dev-github-integration[bot]` unless you add `--act-as-user`.

## Run Claude Code in a throwaway sandbox

For a one-off task, or code you'd rather not run anywhere you care about,
give it a fresh VM and delete the VM afterwards. Claude Code is
preinstalled, and `exeuntu configure claude` connects it to the LLM
integration. On your computer:

```bash
ssh exe.dev new --name=my-claude-task --sandbox
ssh my-claude-task.exe.xyz 'exeuntu configure claude &&
  git clone https://github.com/you/myapp.git && cd myapp &&
  claude -p --dangerously-skip-permissions "Find one bug, fix it, and add a test" &&
  git diff'
ssh exe.dev rm my-claude-task
```

`--sandbox` makes a [standalone VM](/docs/standalone-vms), billed by the
second, that doesn't use your plan's shared capacity; leave it off to use
that capacity instead. For a private repo, add `--integration=myapp` to
`new` and clone from `github.int.exe.xyz` as above. To start sandboxes
from code, one per task, see
[TypeScript sandboxes](/deploy/typescript-sandboxes).

## Make it yours

The app is in `~/claude-agent` on the VM: `agent.ts` (the SDK options and
the web server), `public/index.html` (the page), and `agent.env` (the
model: `sonnet`, `opus`, or `haiku`). After a change, on the VM, run
`sudo systemctl restart claude-agent`.

Because the SDK runs Claude Code, Claude Code's own files work too:
instructions in `~/claude-agent/workspace/CLAUDE.md` and skills in
`~/claude-agent/workspace/.claude/skills/` apply to every conversation.
To change the app, ask the chat (it knows where its code is), or SSH in
and run `claude` in `~/claude-agent`.

## Share it

On your computer:

```bash
ssh exe.dev share add my-claude-agent-sdk ann@example.com
```

Ann signs in at the same address and gets her own conversations, in the
same workspace. Share only with people you'd trust with a terminal on
this VM.

## Keep it safe

Your code and Claude's work are in `~/claude-agent`; conversations are in
`~/.claude/projects`. Copy both **off the VM** now and then. On your
computer:

```bash
mkdir -p claude-agent-backup
rsync -av --exclude node_modules my-claude-agent-sdk.exe.xyz:claude-agent my-claude-agent-sdk.exe.xyz:.claude/projects claude-agent-backup/
```

To restore on a new VM, follow "Install by hand", then put both back. On
your computer:

```bash
rsync -av claude-agent-backup/claude-agent/ my-claude-agent-sdk.exe.xyz:claude-agent/
rsync -av claude-agent-backup/projects/ my-claude-agent-sdk.exe.xyz:.claude/projects/
ssh my-claude-agent-sdk.exe.xyz sudo systemctl restart claude-agent
```

Something odd? On the VM, `journalctl -u claude-agent -n 50` shows what
the app logged.

## The nuts and bolts

Happy clicking Deploy? You can stop here. These are the setup steps
Shelley follows, plus the commands for later.

<details id="create-the-vm" data-manual-setup class="dp-details">
<summary>Install by hand</summary>

### Create a VM

On your computer (pick another name if `my-claude-agent-sdk` is taken):

```bash
ssh exe.dev new --name=my-claude-agent-sdk
ssh my-claude-agent-sdk.exe.xyz
```

All remaining installation commands run **on the VM**, as `exedev`.

### Install Node.js 24

The TypeScript SDK needs Node.js, and Node 24 runs `.ts` files as they
are:

```bash
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejs
node --version
```

Expect a version starting with `v24`.

### Write the app

The project, with the SDK pinned to 0.3.304, and its settings. The
package brings its own Claude Code binary, separate from the VM's
`claude`:

```bash
mkdir -p ~/claude-agent/workspace ~/claude-agent/public
cd ~/claude-agent
cat > package.json <<'EOF'
{
  "name": "claude-agent",
  "private": true,
  "type": "module",
  "dependencies": {
    "@anthropic-ai/claude-agent-sdk": "0.3.304"
  }
}
EOF
npm install --no-audit --no-fund
[ -e agent.env ] || cat > agent.env <<'EOF'
# Read at start: sudo systemctl restart claude-agent after a change.
# Models come from exe.dev's LLM integration; the key is a placeholder it replaces.
ANTHROPIC_BASE_URL=https://llm.int.exe.xyz
ANTHROPIC_API_KEY=implicit
# sonnet, opus, haiku, or a full model name such as claude-sonnet-5-5
CLAUDE_MODEL=sonnet
# gh goes through exe.dev's GitHub integration, if you add one
GH_HOST=github.int.exe.xyz
EOF
```

The app: each message becomes a `query()`, Claude works in `workspace/`
without permission prompts, and every step streams to the browser.
Later messages resume the conversation from its transcript on disk. The
web server answers only requests that come through exe.dev's
[HTTPS proxy](/docs/proxy), which names the signed-in person in
`X-ExeDev-Email`, or from the VM itself:

```bash
cat > ~/claude-agent/agent.ts <<'EOF'
// A Claude Agent SDK app: a private web chat with Claude, working in ./workspace on this VM.
// SDK reference: https://code.claude.com/docs/en/agent-sdk/typescript
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { hostname } from "node:os";
import { join } from "node:path";
import { getSessionMessages, query, type Options } from "@anthropic-ai/claude-agent-sdk";

const dir = import.meta.dirname;
const workspace = join(dir, "workspace");
const vm = hostname();

// Every conversation gets these options. The VM is the sandbox, so Claude acts without asking.
const options: Options = {
  cwd: workspace,
  model: process.env.CLAUDE_MODEL || "sonnet",
  permissionMode: "bypassPermissions",
  allowDangerouslySkipPermissions: true,
  maxTurns: 100,
  systemPrompt: {
    type: "preset",
    preset: "claude_code",
    append: `You run on ${vm}, an exe.dev VM of your own, as exedev with sudo. ` +
      `This chat is ${dir}/agent.ts, served on port 8000 by claude-agent.service; restarting it ends your turn. ` +
      `To show the user a web app, run it as a systemd service on 0.0.0.0, port 3000 to 9999, and link ` +
      `https://${vm}.exe.xyz:<port>/. Only the VM's owner and people they share it with can open it.`,
  },
};

const busy = new Set<string>(); // sessions with a turn in progress

createServer(async (req, res) => {
  // Requests come through exe.dev's proxy, which names the signed-in person, or from the VM itself.
  const local = ["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(req.socket.remoteAddress ?? "");
  if (!local && !req.headers["x-exedev-email"]) {
    res.writeHead(403).end(`Sign in at https://${vm}.exe.xyz/__exe.dev/login?redirect=/\n`);
    return;
  }
  const url = new URL(req.url ?? "/", "http://localhost");
  try {
    if (req.method === "GET" && url.pathname === "/") {
      res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
      res.end(await readFile(join(dir, "public", "index.html")));
    } else if (req.method === "GET" && url.pathname === "/history") {
      res.writeHead(200, { "content-type": "application/json" });
      res.end(JSON.stringify(await history(url.searchParams.get("session") ?? "")));
    } else if (req.method === "POST" && url.pathname === "/chat") {
      let body = "";
      for await (const chunk of req) body += chunk;
      const { message, session } = JSON.parse(body);
      if (session && busy.has(session)) {
        res.writeHead(409).end("Claude is still working on your last message. Reload in a moment.\n");
        return;
      }
      res.writeHead(200, { "content-type": "application/x-ndjson" });
      await chat(String(message), session, (event) => res.write(JSON.stringify(event) + "\n"));
      res.end();
    } else {
      res.writeHead(404).end("Not found\n");
    }
  } catch (err) {
    console.error(err);
    if (!res.headersSent) res.writeHead(500);
    res.end(`${err}\n`);
  }
}).listen(8000, "0.0.0.0", () => console.log(`Claude agent on port 8000, working in ${workspace}`));

// One turn of a conversation: Claude works until it's done, and every step goes to the browser.
// A new conversation starts without a session; later turns resume it from its transcript on disk.
async function chat(message: string, session: string | undefined, send: (event: object) => void) {
  if (session) busy.add(session);
  try {
    for await (const m of query({ prompt: message, options: { ...options, resume: session } })) {
      if (m.type === "system" && m.subtype === "init") {
        session = m.session_id;
        busy.add(session);
        send({ session });
      } else if (m.type === "assistant") {
        for (const block of m.message.content) {
          if (block.type === "text") send({ text: block.text });
          if (block.type === "tool_use") send({ tool: block.name, input: block.input });
        }
      } else if (m.type === "result") {
        send({ done: m.subtype, cost: m.total_cost_usd });
      }
    }
  } catch (err) {
    send({ error: String(err) });
  } finally {
    if (session) busy.delete(session);
  }
}

// A conversation so far, read back from its transcript in ~/.claude/projects.
async function history(session: string) {
  if (!/^[0-9a-f-]{36}$/.test(session)) return [];
  const items: object[] = [];
  for (const m of await getSessionMessages(session, { dir: workspace }).catch(() => [])) {
    const content = (m.message as any)?.content;
    if (m.parent_tool_use_id) continue;
    if (m.type === "user") {
      const text = typeof content === "string" ? content
        : (content ?? []).filter((b: any) => b.type === "text").map((b: any) => b.text).join("");
      if (text && !text.startsWith("<task-notification>")) items.push({ user: text }); // skip Claude Code's own notes
    } else {
      for (const b of content ?? []) {
        if (b.type === "text") items.push({ text: b.text });
        if (b.type === "tool_use") items.push({ tool: b.name, input: b.input });
      }
    }
  }
  if (busy.has(session)) items.push({ working: true });
  return items;
}
EOF
```

The page:

```bash
cat > ~/claude-agent/public/index.html <<'EOF'
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Claude agent</title>
<style>
  body { font: 16px/1.5 system-ui, sans-serif; max-width: 48rem; margin: 0 auto; padding: 0 1rem; }
  header { display: flex; justify-content: space-between; align-items: center; padding: .75rem 0; }
  #log > div { margin: .6rem 0; white-space: pre-wrap; overflow-wrap: anywhere; }
  .user { background: #f0eee6; padding: .5rem .75rem; border-radius: .5rem; }
  .tool { color: #666; font: 13px/1.4 ui-monospace, monospace; }
  .error { color: #b3261e; }
  form { position: sticky; bottom: 0; display: flex; gap: .5rem; padding: .75rem 0; background: #fff; }
  textarea { flex: 1; font: inherit; padding: .5rem; }
</style>
<header><strong>Claude agent</strong><button id="new">New chat</button></header>
<div id="log"></div>
<form id="form">
  <textarea id="input" rows="3" placeholder="Ask Claude to do something on this VM (Enter to send)"></textarea>
  <button>Send</button>
</form>
<script type="module">
const log = document.getElementById("log");
const input = document.getElementById("input");
const form = document.getElementById("form");
let session = localStorage.getItem("session");

function show(item) {
  const div = document.createElement("div");
  if (item.user) { div.className = "user"; div.textContent = item.user; }
  else if (item.text) div.textContent = item.text;
  else if (item.tool) {
    const i = item.input ?? {};
    const what = String(i.command ?? i.file_path ?? i.pattern ?? i.url ?? i.description ?? "");
    const line = what.split("\n")[0].slice(0, 200);
    div.className = "tool";
    div.textContent = `▸ ${item.tool} ${line}${line.length < what.length ? " …" : ""}`;
  }
  else if (item.done) { div.className = "tool"; div.textContent = `✓ ${item.done}, $${(item.cost ?? 0).toFixed(4)}`; }
  else if (item.working) { div.className = "tool"; div.textContent = "… still working. Reload to see more."; }
  else if (item.error) { div.className = "error"; div.textContent = item.error; }
  else return;
  log.append(div);
  div.scrollIntoView({ block: "end" });
}

if (session) for (const item of await (await fetch(`history?session=${session}`)).json()) show(item);

document.getElementById("new").onclick = () => {
  session = null;
  localStorage.removeItem("session");
  log.replaceChildren();
};
input.onkeydown = (e) => {
  if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); form.requestSubmit(); }
};
form.onsubmit = async (e) => {
  e.preventDefault();
  const message = input.value.trim();
  if (!message) return;
  input.value = "";
  show({ user: message });
  const res = await fetch("chat", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ message, session }),
  });
  if (!res.ok) return show({ error: await res.text() });
  const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
  let buffer = "";
  for (let chunk; !(chunk = await reader.read()).done;) {
    const lines = (buffer + chunk.value).split("\n");
    buffer = lines.pop();
    for (const line of lines) {
      const item = JSON.parse(line);
      if (item.session) localStorage.setItem("session", (session = item.session));
      show(item);
    }
  }
};
</script>
</html>
EOF
```

### Start it

systemd starts the app at boot and restarts it whenever it stops:

```bash
sudo tee /etc/systemd/system/claude-agent.service >/dev/null <<'EOF'
[Unit]
Description=Claude Agent SDK app
Wants=network-online.target
After=network-online.target

[Service]
User=exedev
WorkingDirectory=/home/exedev/claude-agent
EnvironmentFile=/home/exedev/claude-agent/agent.env
Environment=PATH=/home/exedev/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/usr/bin/node agent.ts
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now claude-agent
until curl -fso /dev/null localhost:8000/; do sleep 1; done
systemctl is-enabled claude-agent
```

Expect `enabled`. Now ask Claude something:

```bash
curl -sN localhost:8000/chat -H 'Content-Type: application/json' \
  -d '{"message":"Reply with exactly: pong"}'
```

Expect `{"session":"…"}`, `{"text":"pong"}`, and
`{"done":"success","cost":…}`, one per line.

### Check it remembers

Start a conversation, restart the app, and carry on with it:

```bash
session=$(curl -sN localhost:8000/chat -H 'Content-Type: application/json' \
  -d '{"message":"Remember the word kiwi. Reply with exactly: ok"}' | head -1 | jq -r .session)
sudo systemctl restart claude-agent
until curl -fso /dev/null localhost:8000/; do sleep 1; done
curl -sN localhost:8000/chat -H 'Content-Type: application/json' \
  -d '{"message":"Which word did I ask you to remember?","session":"'"$session"'"}'
```

Expect a reply with `kiwi` in it.

</details>

<details id="update" class="dp-details">
<summary>Update</summary>

The SDK changes often, and it's pre-1.0, so read the
[changelog](https://github.com/anthropics/claude-agent-sdk-typescript/blob/main/CHANGELOG.md)
first. Then, on the VM:

```bash
cd ~/claude-agent
npm install @anthropic-ai/claude-agent-sdk@latest --no-audit --no-fund
sudo systemctl restart claude-agent
```

Run the pong check from "Start it" again. To go back, put the old
version in `package.json` and run `npm install`.

</details>

<details id="troubleshooting" class="dp-details">
<summary>Troubleshooting</summary>

On the VM, `journalctl -u claude-agent -n 50` shows the app's errors.

- **The page says "Sign in at…"**: the request didn't come through the
  exe.dev proxy with a signed-in person. Open the link it gives.
- **Replies fail with an authentication or model error**: check that the
  LLM integration reaches this VM with
  `curl -s https://reflection.int.exe.xyz/integrations`, and that
  `CLAUDE_MODEL` in `agent.env` is a model it offers.
- **"Claude is still working on your last message"**: a turn is still
  running. Reload the page to see how far it got, and send your message
  when it's done.

</details>

## Frequently asked questions

### What's the difference between the Claude Agent SDK and Claude Code?

The SDK is Claude Code as a library: the same agent loop and tools, driven by your code instead of a terminal. The npm package carries its own Claude Code binary and runs it as a subprocess for each conversation. Want Claude Code in a terminal instead? It's preinstalled: see [Claude Code on exe.dev](/deploy/claude-code).

### Do I need an Anthropic API key?

No. Requests go through exe.dev's [LLM integration](/docs/integrations-llm) and are billed to your exe.dev account. To use your own key, set `ANTHROPIC_API_KEY` to it in `~/claude-agent/agent.env`, delete the `ANTHROPIC_BASE_URL` line, and run `sudo systemctl restart claude-agent`.

### Is it safe to let Claude run commands without asking?

On this VM, yes, within limits: that's what the sandbox is for. Claude works as `exedev`, not root, on a machine with none of your laptop's files or keys, and the LLM integration keeps the model key off it. The worst case is a broken VM. Don't put secrets on it you wouldn't hand Claude, and keep it private.

### How is this different from E2B, Daytona, or Vercel Sandbox?

Those hand your code short-lived containers to drive through an API. This VM persists: its disk, its HTTPS address, and the apps Claude starts are still there tomorrow. For short-lived runs, `new --sandbox` makes a VM [billed by the second](/docs/standalone-vms), and the [TypeScript SDK](/deploy/typescript-sandboxes) drives them from code.

### Can I write the agent in Python?

Yes. The Python package is `claude-agent-sdk` (`uv add claude-agent-sdk`), with the same options in snake_case. Ask the chat to port `agent.ts`; it can see its own code.

### What does it cost?

Your [VM plan](https://exe.dev/pricing) plus model usage, which the page shows after every reply. `maxTurns: 100` in `agent.ts` stops a runaway task; Anthropic suggests about 1 GiB of memory and one CPU per agent working at once.

### Can several people use it?

Yes. [Share the VM](#share-it): each browser keeps its own conversations, and everyone's Claude works in the same workspace.


## Related

- [Run Claude Code on a remote server](https://exe-staging.dev/deploy/claude-code.md): A computer for Claude. A breather for your laptop.
- [Create sandboxes from TypeScript](https://exe-staging.dev/deploy/typescript-sandboxes.md): Full VMs as sandboxes. Create, control, and destroy them from your code.
- [Host a Pi Durable agent](https://exe-staging.dev/deploy/pi-durable.md): An agent that picks up where it left off, even after kill -9.
- [Running Agents](https://exe-staging.dev/deploy/agents.md): Use exe.dev VMs as a sandbox
- [Host OpenClaw](https://exe-staging.dev/deploy/openclaw.md): An AI sidekick with its own computer. Your laptop can take a nap.
- [Run Codex on a remote server](https://exe-staging.dev/deploy/codex.md): A computer for Codex. An early night for your laptop.
