| Use | Primitive | Why |
| One-shot command, wait for the result | exec.run() | Simplest call. Returns once the command exits. Each call is isolated — no state carries between calls. |
| Long-running / streaming process | exec.start() / exec.background() | Returns a session handle you can stream from, send stdin to, or reattach to later. Use for servers, builds, and any job you need to observe live. |
| Multi-step workflow, state must persist | exec.shell() | Opens a long-lived bash whose cwd, exported env, and shell functions persist across .run() calls — the ergonomics of a terminal tab. Reconnectable via exec.reattachShell(sessionId). |
run().start() / background().shell().shell() is an exec session — its sessionId shows up in exec.list() next to anything started via start(). The difference is purely how the SDK frames commands on top: a sentinel protocol per .run() call so you can treat one long-lived bash as many discrete calls.123456import { Sandbox } from "@opencomputer/sdk"; const sandbox = await Sandbox.create(); const result = await sandbox.exec.run("echo Hello, World!"); console.log(result.stdout); // "Hello, World!\n" await sandbox.kill();
exec.run()sh -c, so pipes, redirects, and shell expansion work.12345678910// Working directory and environment variables const result = await sandbox.exec.run("npm run build", { cwd: "/app", env: { NODE_ENV: "production" }, timeout: 120, }); if (result.exitCode !== 0) { console.error("Build failed:", result.stderr); }
| Parameter | Type | Default | Description |
command | string | — | Shell command to run (required) |
timeout | number | 60 | Timeout in seconds |
env | object | — | Environment variables |
cwd | string | — | Working directory |
| Field | TypeScript | Python | Type |
| Exit code | exitCode | exit_code | number |
| Standard output | stdout | stdout | string |
| Standard error | stderr | stderr | string |
exec.start() / exec.background()ExecSession with callbacks for stdout, stderr, and exit. exec.background() is an alias for exec.start() — same options, same return type.1234567891011121314const session = await sandbox.exec.start("node server.js", { cwd: "/app", env: { PORT: "3000" }, onStdout: (data) => process.stdout.write(data), onStderr: (data) => process.stderr.write(data), onExit: (code) => console.log("Server exited:", code), maxRunAfterDisconnect: 300, // keep running 5min after disconnect }); // Send input session.sendStdin("some input\n"); // Wait for completion (or kill) await session.kill();
| Parameter | TypeScript | Python | Description |
| Arguments | args: string[] | args: list[str] | Command arguments |
| Env | env: object | env: dict | Environment variables |
| Working dir | cwd: string | cwd: str | Working directory |
| Timeout | timeout: number | timeout: int | Timeout in seconds |
| Keep-alive | maxRunAfterDisconnect | max_run_after_disconnect | Seconds to keep running after disconnect |
| Stdout | onStdout | on_stdout | Callback receiving raw bytes |
| Stderr | onStderr | on_stderr | Callback receiving raw bytes |
| Exit | onExit | on_exit | Callback receiving the exit code |
| Member | TypeScript | Python | Description |
| Session ID | sessionId | session_id | Session identifier |
| Done | done: Promise<number> | done: asyncio.Future[int] | Resolves with exit code |
| Send stdin | sendStdin(data) | send_stdin(data) | Write to the process |
| Kill | kill(signal?) | kill(signal=9) | Kill the process |
| Detach | close() | close() | Close the WebSocket (process keeps running) |
exec.shell()bash session whose state (cwd, exported env vars, shell functions) persists across .run() calls. Foreground-only: concurrent .run() rejects with ShellBusyError.12345678910const sh = await sandbox.exec.shell({ cwd: "/app" }); await sh.run("npm install"); await sh.run("export NODE_ENV=test"); const r = await sh.run("npm test", { onStdout: (b) => process.stdout.write(b), }); console.log(r.exitCode); await sh.close();
sessionId is stable across SDK invocations. Keep the id and revisit the same shell later — cwd, env, and shell functions are still there because the bash process never went anywhere.123456789const sh = await sandbox.exec.shell(); await sh.run("cd /srv && export DEPLOY=canary"); const id = sh.sessionId; // persist this somewhere // ...later, possibly a different process... const sh2 = await sandbox.exec.reattachShell(id); const r = await sh2.run("pwd; echo $DEPLOY"); // → "/srv\ncanary\n"
.run() from another client). If two clients try to drive the same shell concurrently, their output will interleave — coordinate at the application level.exit (or exit N) inside sh.run() closes the shell — same as closing a terminal tab. The pending .run() rejects with ShellClosedError and any subsequent .run() on the same Shell also rejects. Start a fresh shell() if you need another one.onStdout/onStderr to sh.run() and they fire as bytes arrive, before the promise resolves. Two caveats worth knowing:echo, printf, and other builtins go through glibc stdio, which only flushes when the buffer fills (~4 KB) or bash exits. If you want live output from a simple loop, use an external binary (/bin/echo) or a tool that flushes explicitly (python -u, stdbuf -oL <cmd>).sdks/typescript/examples/stream-demo.ts / sdks/python/examples/stream_demo.py for a ~6-second apt-install simulation that prints per-chunk arrival timestamps — a good way to eyeball streaming behavior against your deployment.cwd, env, and timeout are intentionally not supported in v1. Use inline shell syntax (cd /x && cmd, FOO=bar cmd) — the shell state carries across calls. Use exec.background() for fire-and-forget processes.12345const sessions = await sandbox.exec.list(); for (const s of sessions) { console.log(s.sessionID, s.running ? "running" : `exited (${s.exitCode})`); console.log(` command: ${s.command}, clients: ${s.attachedClients}`); }
123456const session = await sandbox.exec.attach(sessionId, { onStdout: (data) => process.stdout.write(data), onStderr: (data) => process.stderr.write(data), onExit: (code) => console.log("Exited:", code), onScrollbackEnd: () => console.log("--- live output ---"), });
12await sandbox.exec.kill(sessionId); await sandbox.exec.kill(sessionId, 15); // SIGTERM
sandbox.commands is a deprecated alias for sandbox.exec. Use sandbox.exec in all new code.