Core conceptsBeta

Shell

Add a bash shell to a session, run commands with exec and work interactively in a terminal.

Add "shell": true when you create a session and it gets a bash shell next to the browser, on the same machine and the same /workspace disk. There is one shell machine, with Python (numpy, pandas, requests), Node (TypeScript, tsx, pnpm, yarn), ffmpeg, ImageMagick and poppler-utils already installed. For a shell without a browser, also pass "browser": false.

Shell
curl -X POST https://api.staging.boxline.dev/v1/sessions \
  -H "x-api-key: $BOXLINE_API_KEY" \
  -H "content-type: application/json" \
  -d '{"browser": true, "shell": true}'

Sessions with a shell keep running until you release them or they reach their timeout, even when no browser client is connected.

Run a command

POST/v1/sessions/:id/exec

Shell
curl -X POST https://api.staging.boxline.dev/v1/sessions/$SESSION_ID/exec \
  -H "x-api-key: $BOXLINE_API_KEY" \
  -H "content-type: application/json" \
  -d '{"command": "ls -la downloads", "timeoutMs": 30000}'
FieldDescription
commandThe bash command to run. Required.
timeoutMsHow long to let it run, in milliseconds. Default 120,000, maximum 3,600,000.
cwdWorking directory for this command, inside the workspace.
envExtra environment variables for this command.
shellWhich persistent shell to use: "default", any other name to get a separate one, or false to run in a fresh process that keeps no state.
streamtrue to stream output as NDJSON (see streaming).
Response
{
  "stdout": "report.csv\n",
  "stderr": "",
  "exitCode": 0,
  "timedOut": false,
  "truncated": false,
  "durationMs": …
}

exitCode is null when the command was stopped by its timeout (timedOut: true). truncated means the output was too long to return in full; write big outputs to a file and read it with the files API.

A persistent shell

Commands run in one persistent bash session that starts in /workspace, not a fresh shell each time. cd and export carry over between calls, as they would in a terminal:

Shell
# call 1
cd downloads && export REPORT=report.csv
# call 2: still in /workspace/downloads, $REPORT still set
python3 ../summarise.py "$REPORT"

If the shell gets into a bad state, restart it with POST/v1/sessions/:id/shell/restart (body { shell? }, returns 204), or session.shell.restart() in the SDK. Files in the workspace are not affected.

Streaming output

With "stream": true, the response is newline-delimited JSON: one line per chunk of output, then a final exit line.

Stream (NDJSON)
{"type":"stdout","data":"Collecting pandas\n"}
{"type":"stderr","data":"WARNING: …\n"}
{"type":"exit","exitCode":0,"timedOut":false,"durationMs":…,"truncated":false}

The SDK handles the parsing:

TypeScript
const exit = await session.execStream(
  "pip install -r requirements.txt",
  (stream, data) => (stream === "stdout" ? process.stdout : process.stderr).write(data),
  { timeoutMs: 300_000 },
);
console.log("exit code", exit.exitCode);

Interactive terminal

terminalUrl on the session is a signed WebSocket attached to a real terminal on the machine. Send text frames of JSON, either {"type":"input","data":"ls\r"} or {"type":"resize","cols":120,"rows":32}; the server sends raw terminal output as binary frames, ready for a terminal emulator such as xterm.js.

Reuse the browser login in the shell

Signed in with the browser and want to call the same site from the shell? Export the browser’s cookies as a Netscape cookie file that curl and wget understand:

POST/v1/sessions/:id/browser/cookies/export

TypeScript
const { path, count } = await session.browser.exportCookies(); // /workspace/cookies.txt
await session.exec("curl -b cookies.txt -o downloads/invoices.json https://example.com/api/invoices");

The body takes an optional path (default cookies.txt in the workspace) and the response is { path, count }. The file holds live credentials, so delete it when you are done.

Use it as Claude’s bash tool

Claude’s bash tool asks your code to run commands. Point it at a session and every command runs in an isolated, persistent shell instead of on your own machine. This loop also handles restarts, refusals (with server-side fallbacks) and paused turns:

agent.ts
import Anthropic from "@anthropic-ai/sdk";
import { Boxline } from "@boxline/sdk";

const anthropic = new Anthropic();
const client = new Boxline();
const session = await client.sessions.create({
  browser: false,
  shell: true,
});

const messages: Anthropic.Beta.BetaMessageParam[] = [
  { role: "user", content: "Use Python to list the 10 largest files under /usr/lib." },
];

try {
  while (true) {
    const res = await anthropic.beta.messages.create({
      model: "claude-opus-5",
      max_tokens: 16000,
      betas: ["server-side-fallback-2026-07-01"],
      fallbacks: "default",
      tools: [{ type: "bash_20250124", name: "bash" }],
      messages,
    });
    if (res.stop_reason === "refusal") break;
    if (res.stop_reason === "pause_turn") {
      messages.push({ role: "assistant", content: res.content });
      continue;
    }
    const calls = res.content.filter(
      (b): b is Anthropic.Beta.BetaToolUseBlock => b.type === "tool_use",
    );
    if (calls.length === 0) break;
    messages.push({ role: "assistant", content: res.content });

    // Each bash call runs in the session's persistent shell
    const results: Anthropic.Beta.BetaToolResultBlockParam[] = [];
    for (const call of calls) {
      const input = call.input as { command?: string; restart?: boolean };
      if (input.restart) {
        await session.shell.restart();
        results.push({ type: "tool_result", tool_use_id: call.id, content: "Shell restarted." });
        continue;
      }
      const run = await session.exec(input.command ?? "", { timeoutMs: 120_000 });
      results.push({
        type: "tool_result",
        tool_use_id: call.id,
        content: run.stdout + run.stderr || "(no output)",
        is_error: run.exitCode !== 0,
      });
    }
    messages.push({ role: "user", content: results });
  }
} finally {
  await session.release();
}

Install packages

Every session is its own sandbox, and you have sudo inside it, so you can install anything: sudo apt-get install -y ffmpeg, pip install yt-dlp, npm install sharp. Python, Node, ffmpeg, ImageMagick and poppler-utils are already there.

To have packages ready every time, pass setup when you create the session. The commands run in order in the session’s shell, and your own commands wait until they finish.

Shell
curl -X POST https://api.staging.boxline.dev/v1/sessions \
  -H "x-api-key: $BOXLINE_API_KEY" \
  -H "content-type: application/json" \
  -d '{"shell": true,
       "setup": ["sudo apt-get install -y ffmpeg", "pip install yt-dlp"]}'

The session reports setupStatus (running, done or failed, with setupError), and each step appears in the session’s events.

These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.