Documentation Index
Fetch the complete documentation index at: https://docs.opencomputer.dev/llms.txt
Use this file to discover all available pages before exploring further.
Three primitives, each tuned for a different shape of work:
| 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). |
Rules of thumb:
- Need a return value and don’t care about streaming? →
run().
- Starting something that outlives the call (a server, a log tailer, a slow build)? →
start() / background().
- Doing a sequence of commands where one step’s state (directory, env var, activated venv) affects the next? →
shell().
Under the hood 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.
import { 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();
Quick Commands: exec.run()
Run a shell command and wait for the result. The command runs via sh -c, so pipes, redirects, and shell expansion work.
// 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);
}
Parameters
| 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 |
ProcessResult
| Field | TypeScript | Python | Type |
|---|
| Exit code | exitCode | exit_code | number |
| Standard output | stdout | stdout | string |
| Standard error | stderr | stderr | string |
Async Commands: exec.start() / exec.background()
Start a command as an exec session for long-running processes or streaming output. Returns an ExecSession with callbacks for stdout, stderr, and exit. exec.background() is an alias for exec.start() — same options, same return type.
const 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();
ExecStartOpts
| 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 |
ExecSession
| 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) |
Stateful Shell: exec.shell()
Open a long-lived bash session whose state (cwd, exported env vars, shell functions) persists across .run() calls. Foreground-only: concurrent .run() rejects with ShellBusyError.
const 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();
Reattaching to an open shell
The shell is just an exec session, so its 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.
const 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"
Reattach assumes the shell is idle (no in-flight .run() from another client). If two clients try to drive the same shell concurrently, their output will interleave — coordinate at the application level.
Terminal-tab semantics
Running 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.
Streaming output
Pass onStdout/onStderr to sh.run() and they fire as bytes arrive, before the promise resolves. Two caveats worth knowing:
- bash builtin output is block-buffered.
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>).
- Upstream hops may coalesce small frames. Chunks travel bash → agent → worker (gRPC) → WS → CDN → client. Any of those hops is allowed to combine small frames under light load, so a short command may arrive as one chunk even if its output was produced incrementally. Real workloads (builds, installs, servers) produce enough output that streaming is visible in practice.
See 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.
Per-call 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.
Managing Sessions
List Sessions
const 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}`);
}
Attach to Running Session
Reconnect to a running exec session to resume streaming output:
const 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 ---"),
});
On attach, the server replays the scrollback buffer (historical output), sends a scrollback-end marker, then streams live output.
Kill a Session
await 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.