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.
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
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}'| Field | Description |
|---|---|
command | The bash command to run. Required. |
timeoutMs | How long to let it run, in milliseconds. Default 120,000, maximum 3,600,000. |
cwd | Working directory for this command, inside the workspace. |
env | Extra environment variables for this command. |
shell | Which 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. |
stream | true to stream output as NDJSON (see streaming). |
{
"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:
# 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.
{"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:
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
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:
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.
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.