Docs / Hotcell / Working in a sandbox

Files, processes, sessions and ports

Run commands, manage files, start background processes, keep sessions, expose preview URLs and run code in a Hotcell sandbox.

Each section shows the CLI, the TypeScript SDK and the REST endpoint. Python method names are the snake_case equivalents (write_file, start_process and so on). See the SDK reference and REST API reference for full signatures.

Exec

hotcell exec <id> "npm test" --cwd /workspace/app --env CI=1
const r = await sandbox.exec("npm test", { cwd: "/workspace/app", env: { CI: "1" } });
console.log(r.exitCode, r.success, r.stdout);

for await (const ev of sandbox.execStream("npm test")) {
  if (ev.type === "stdout") process.stdout.write(ev.data);
}

POST /sandboxes/:id/exec with { command, cwd?, env?, sessionId? } streams Server-Sent Events. Each frame is one JSON event:

data: {"type":"stdout","data":"hello\n"}

data: {"type":"exit","exitCode":0}

Event types are stdout, stderr and exit. The daemon sends an SSE comment frame (: hb) every 15 seconds on a quiet stream, so client timeouts don't kill long-running commands that print nothing.

Environment precedence for a command: sandbox env, then session env, then the request's env.

Environment variables

hotcell env <id> NODE_ENV=production     # set
hotcell env <id>                         # print
await sandbox.setEnvVars({ NODE_ENV: "production" });   // merged into the sandbox env
const env = await sandbox.getEnvVars();

Sessions

A session keeps a working directory and an env overlay. A cd in one command persists to the next.

hotcell session create <id> --cwd /workspace/app --id build
hotcell exec <id> "cd src" --session build
hotcell exec <id> "pwd" --session build      # /workspace/app/src
hotcell session ls <id>
hotcell session rm <id> build
const s = await sandbox.createSession({ cwd: "/workspace/app" });
await s.exec("cd src");
await s.exec("pwd");
await s.setEnvVars({ DEBUG: "1" });
await s.destroy();

The default session working directory is /workspace. Sessions survive a stop.

Files

hotcell files write <id> /workspace/a.txt --content "hello" --mode 600
hotcell files read  <id> /workspace/a.txt
hotcell files mkdir <id> /workspace/out --parents
hotcell files ls    <id> /workspace
await sandbox.writeFile("/workspace/a.txt", "hello", { mode: "600" });
const text = await sandbox.readFile("/workspace/a.txt");
await sandbox.mkdir("/workspace/out", { parents: true });
const entries = await sandbox.listFiles("/workspace");   // { path, name, isDirectory, size, modifiedAt }

File content is UTF-8 text. mode is a 3- or 4-digit octal string. Request bodies are capped by HOTCELL_MAX_BODY_BYTES (default 32 MiB).

Watch for changes

hotcell watch <id> /workspace --interval 500
for await (const ev of sandbox.watch("/workspace")) {
  console.log(ev.type, ev.path);        // created | modified | deleted
}

GET /sandboxes/:id/watch?path=/workspace&interval=<ms> streams SSE events until you close the connection. Watching is recursive.

Background processes

hotcell start <id> "python3 -m http.server 8000" --cwd /workspace
hotcell ps <id>
hotcell logs <id> <procId> --follow
hotcell kill <id> <procId> --signal SIGINT
const p = await sandbox.startProcess("python3 -m http.server 8000");
for await (const line of sandbox.streamLogs(p.procId, { follow: true })) console.log(line);
await sandbox.killProcess(p.procId);    // SIGTERM by default

A process handle has procId, pid, command, status (running or exited), exitCode, startedAt and logPath. On the container driver, background processes end when the sandbox stops or pauses; on microVM drivers they survive a pause.

Wait for a port

hotcell wait-port <id> 8000 --timeout 30000
const ready = await sandbox.waitForPort(8000, { timeoutMs: 30_000 });

Returns whether a TCP port is listening inside the sandbox before the timeout. You can pass host to probe a specific address and intervalMs to set the polling interval.

Preview URLs

hotcell expose <id> 8000 --token s3cret
const { url } = await sandbox.exposePort(8000, { token: "s3cret" });
// http://<id>-8000.localhost:4751/
await sandbox.listExposedPorts();
await sandbox.unexposePort(8000);

The preview proxy listens on HOTCELL_PROXY_PORT (default 4751). URLs use a *.localhost subdomain, which browsers resolve to 127.0.0.1 without DNS setup. The proxy works at layer 4, so it works where container IPs aren't reachable from the host (Docker Desktop on macOS).

If you set a token, requests must carry it as a ?token= query parameter or an Authorization: Bearer <token> header; otherwise the proxy returns 403. Inbound traffic on a preview URL resumes a paused sandbox.

Code interpreter

Stateful Python and JavaScript contexts. Variables and imports persist across runs in the same context.

hotcell run-code <id> "print(21 * 2)" --lang python
const ctx = await sandbox.createCodeContext({ language: "python" });
await ctx.runCode("x = 21");
const r = await ctx.runCode("x * 2");
console.log(r.results[0].text);      // "42"
await ctx.destroy();

// one-off run in a throwaway kernel
await sandbox.runCode("console.log(1 + 1)", { language: "javascript" });

A result has stdout, stderr, results (text outputs) and error. The default timeout is 30 seconds (timeoutMs). Contexts end when the sandbox stops, and on a container pause.

Interactive terminal

hotcell terminal <id>

This attaches a PTY to the sandbox in your local terminal. The dashboard offers the same shell through xterm.js. Both use the WebSocket endpoint GET /sandboxes/:id/terminal?cols=<n>&rows=<n>: binary frames carry stdin and output, and text frames carry JSON control messages such as {"type":"resize","cols":120,"rows":40}. Browsers can't set headers on a WebSocket, so the key can also be passed as ?key=.

Metrics

hotcell stats <id>
const m = await sandbox.metrics();        // { status, live, usage, cost }
const samples = await sandbox.metricsHistory();

See Observability.