Skip to content
Sandbox SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Running commands

Run shell commands, stream their output, and build commands safely with the $ tag. Covers defaults, timeouts, and what a result contains.

box.run runs one shell command and resolves with its result once the command exits. A non-zero exit code is a normal result, not an error. The SDK throws only when the sandbox or the provider fails.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";

await using box = await createSandbox(local());

const result = await box.run("npm test", { cwd: "app", timeoutMs: 300_000 });

if (!result.ok) {
  console.error(`exit ${result.exitCode}`, result.output);
}

result.ok is true only when the command exited with code 0 and did not time out.

What a result contains

Field Meaning
ok true when exitCode is 0
exitCode The exit code, or null if the command was killed
stdout, stderr Each stream on its own
output stdout and stderr interleaved in the order they arrived
durationMs Wall time of the command
truncated true if output passed maxOutputBytes
timedOut true if timeoutMs ran out
cwd The absolute directory the command ran in
command The command string

Show output to people and models. It reads like a terminal.

Defaults

Every command gets these, so a command can’t hang on a prompt or flood your server:

  • Timeout. 120 seconds. Change it with timeoutMs.
  • Output cap. 1 MB per stream. Past that, the result keeps the start and the end, puts a marker in the middle, and sets truncated: true. Change it with maxOutputBytes.
  • No stdin. stdin is closed unless you pass stdin.
  • Non-interactive env. CI=1, PAGER=cat, GIT_TERMINAL_PROMPT=0, DEBIAN_FRONTEND=noninteractive, TERM=dumb, and NO_COLOR=1 are always set. SANDBOX_SDK_RUN=1 is set too, so idle reaping can tell SDK commands apart from other processes.
  • Working directory. /workspace. A relative cwd resolves against it.
  • No carried state. Each call is a fresh shell. A cd or export in one call does not affect the next. Pass cwd and env instead.

Timeouts and cancellation

When timeoutMs runs out, the SDK kills the whole process group in the sandbox and resolves with timedOut: true, exitCode: null, ok: false, and the output so far. It does not throw.

signal does the same for an AbortSignal. Use it to stop a command when a user cancels a request.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";

await using box = await createSandbox(local());

const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);

const result = await box.run("sleep 60", { signal: controller.signal });
console.log(result.timedOut, result.exitCode);

This prints true null after five seconds, and the sleep process in the sandbox is gone.

Stream output

The value run returns is both a promise and an async iterable. Iterate it to get output as it arrives.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";

await using box = await createSandbox(local());

for await (const chunk of box.run("npm install", { cwd: "app" })) {
  process.stdout.write(`[${chunk.stream}] ${chunk.text}`);
}

Each chunk has stream ("stdout" or "stderr") and text. To stream and also get the final result, keep the handle and await it after the loop:

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";

await using box = await createSandbox(local());

const install = box.run("npm install", { cwd: "app" });
for await (const chunk of install) process.stdout.write(chunk.text);
const result = await install;
console.log(result.ok);

Build commands with $

Never paste user input into a command string. Use the $ tag: it shell-escapes every interpolated value.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";

await using box = await createSandbox(local());

const branch = "feature/it's; rm -rf /";
const result = await box.$`git checkout -b ${branch}`.cwd("app");
console.log(result.exitCode);

branch reaches git as one argument, quotes and semicolon included. $ returns the same handle as run, with chainable .cwd(path), .env(vars), and .timeout(ms). You can await it or iterate it.

Secrets

Values you pass as secrets to createSandbox are set as environment variables in the sandbox and removed from every result, log, and error the SDK produces.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";

const token = process.env.NPM_TOKEN ?? "";

await using box = await createSandbox(local(), { secrets: { NPM_TOKEN: token } });

const result = await box.run("echo $NPM_TOKEN");
console.log(result.stdout);

This prints a redaction marker, not the token. Use env for values that are safe to show.

For long-running commands like dev servers, use processes instead of run.

Was this page helpful?