Execution

There are two ways to run code in a sandbox:

  • exec, run a command, wait for it to finish, get the buffered output back. Best for short, self-contained commands.
  • execStream, start a process and interact with it while it runs: read its output incrementally, and write to its stdin. Best for long-running commands, live output, and interactive sessions.

exec, one-shot

exec resolves once the process exits and returns stdout, stderr, and exit_code. Pass the command as an array of arguments (argv), it runs directly with no shell, so there's no quoting, word-splitting, or glob surprises even when arguments contain spaces.

index.ts
const result = await sandbox.exec(["python3", "-c", "print('hello', 6 * 7)"], {
  cwd: "/workspace",
  env: { PYTHONUNBUFFERED: "1" }, // merged on top of the sandbox's env
});

console.log(result.stdout.trim()); // "hello 42"
console.log(result.exit_code);     // 0

Arguments with spaces are just separate array elements, no escaping needed:

index.ts
await sandbox.exec(["git", "commit", "-m", "my message with spaces"]);
i
A plain string is also accepted and runs through a shell (sh -c), handy when you actually want shell features like pipes or variable expansion ("cat log | grep err"). Prefer the array form otherwise.

execStream, streaming

execStream returns a handle immediately. Read output as it arrives, and await the exit code when the process finishes.

index.ts
const proc = await sandbox.execStream(["npm", "test"], { cwd: "/workspace" });

for await (const chunk of proc.pipes) {
  if (chunk.stdout) process.stdout.write(chunk.stdout);
  if (chunk.stderr) process.stderr.write(chunk.stderr);
}

console.log("exit code:", await proc.exitCode);

Passing stdin to a running process

The streaming handle lets you write to the process's stdin while it runs. This is what makes long-lived, stateful sessions possible: start a process once, then feed it input over time. The handle is ready as soon as execStream resolves, so it's safe to write immediately.

TypeScriptPythonGo
Write stdinproc.writeStdin(data)proc.write_stdin(data)proc.WriteStdin(ctx, data)
Read outputfor await (const c of proc.pipes)async for c in proc.pipesfor c := range proc.Output
Await exitawait proc.exitCodeawait proc.exit_codeproc.Wait()

Here a single Python REPL stays alive across several writes, keeping a running total in memory, the interpreter starts once and is reused:

index.ts
const proc = await sandbox.execStream(["python3", "-iq"], { cwd: "/workspace" });

const commands = [
  "total = 0",                 // setup runs once...
  "total += 5; print(total)",  // ...and state persists across writes
  "total += 6; print(total)",
  "exit()",
];
for (const cmd of commands) {
  await proc.writeStdin(cmd + "\n");
}

for await (const chunk of proc.pipes) {
  if (chunk.stdout) process.stdout.write(chunk.stdout); // 5, then 11
}

console.log("exit code:", await proc.exitCode);
i
Include the newline (\n), most programs read stdin line by line and won't act on input until they see one. For interactive REPLs, a carriage return (\r) works too.

Why sessions matter

Because the process stays running, expensive setup happens once and is reused by every later write, a launched browser, a loaded ML model, an open database connection. Instead of paying that cost per command, you pay it once and stream work into the live process. A Playwright script can launch Chromium a single time and drive many page loads through the same browser; a REPL can import heavy libraries once and reuse them across dozens of evaluations.

Interactive terminals (TTY)

Pass tty: true to allocate a pseudo-terminal. Programs then behave as they would in a real terminal (colors, prompts, line editing). With a TTY, stderr is merged into stdout, so all output arrives on the stdout channel.

index.ts
const proc = await sandbox.execStream(["bash"], { tty: true });
await proc.writeStdin("ls -la\n");

Resizing: write a CSI 8 sequence to stdin to resize the PTY, \x1b[8;<rows>;<cols>t. For example, \x1b[8;40;120t sets 40 rows × 120 columns.

Attaching to the entrypoint's terminal: if the sandbox was created with tty: true in its config, call execStream with an empty command to attach directly to the entrypoint process's terminal instead of spawning a new one. See Pseudo Terminals for a complete terminal implementation, including a TUI (Claude Code) example.

Options

OptionexecexecStreamNotes
cwd✓✓Working directory. Defaults to the sandbox's.
env✓✓Merged on top of the sandbox config's env, overriding same-named entries.
tty—✓Allocate a pseudo-terminal.
signal / timeoutMs✓✓(TypeScript) abort or time-bound the call.

Both exec and execStream accept cwd and env; execStream also takes tty.


Next: Pseudo Terminals