Runtime

How to run Bash scripts and shell commands in a sandbox

Send the command or script to a Linux microVM and run it there with bash, a time limit and network rules, never on your own machine.

On Runtime every command already runs in bash inside its own microVM, with nothing to install. A string sent to exec runs under bash -c as the user runtime, with passwordless sudo, in a Firecracker microVM running Ubuntu 24.04.5. Root in there still cannot change the sandbox's network, CPU, memory or cost (security). A new sandbox took 351 ms from the create request to its first Python result at the median, measured on 24 September 2026 (speed).

Four ways to run shell code

You have Call What runs it
A one-line command exec("ls *.txt && date") bash -c, so globs, && and pipes work
A program and its arguments exec(["grep", "-rn", pattern, "src"]) The program directly, no shell
A script file files.write then exec(["bash", "job.sh"]) bash, reading the file
Commands that share state interpreter.run(code, { language: "bash" }) A bash session that persists

Use the array form whenever part of the command comes from a user or a model. With no shell, a value such as ; rm -rf ~ is only an argument (run commands).

The image has bash, git, curl, wget, jq, rg, fd, sqlite3, zip, unzip, xz, gcc and make (the sandbox environment). Ubuntu 24.04's bash package is 5.2.21 (packages.ubuntu.com, 25 September 2026).

Run a script an agent wrote

Write the script as a file and run it with strict mode on, so the first failing line stops it and the exit code says so.

TypeScriptimport { Sandbox } from "withruntime";const script = `set -euo pipefailmkdir -p outfor n in 1 2 3; do echo "item $n" >> out/list.txt; donewc -l < out/list.txt`;await using sbx = await Sandbox.create({  network: { internet: false },  timeoutSeconds: 300,  onLeaseEnd: "stop",});await sbx.files.write("/workspace/job.sh", script);const run = await sbx.exec(["bash", "job.sh"], { timeoutMs: 30_000 });console.log(run.exitCode, run.timedOut, run.stdout, run.stderr);
Pythonfrom withruntime import Sandboxscript = """set -euo pipefailmkdir -p outfor n in 1 2 3; do echo "item $n" >> out/list.txt; donewc -l < out/list.txt"""with Sandbox.create(network={"internet": False}, timeout_seconds=300, on_lease_end="stop") as sbx:    sbx.files.write("/workspace/job.sh", script)    run = sbx.exec(["bash", "job.sh"], timeout_ms=30_000)    print(run.exit_code, run.timed_out, run.stdout, run.stderr)

What comes back, and what protects you:

  • Exit code, stdout and stderr separately, for the agent to read.
  • A time limit: timeoutMs, 60 seconds by default and 24 hours at most. A script that hangs returns timedOut: true with its output so far.
  • An output cap: at most 64 KiB of stdout and 64 KiB of stderr per result, with stdoutTruncated saying when some was dropped. For more, redirect to a file and read it with files.readText.
  • No route out with internet: false, and private addresses are refused even with the internet on.

Pass secrets and input safely

Put a secret in env, never in the command text. It is never echoed back, and journals record a hash, not the value. stdin feeds the command, then closes.

TypeScriptimport { Sandbox } from "withruntime";await using sbx = await Sandbox.create();const count = await sbx.exec(["bash", "-c", 'grep -c "$1"', "count", "error"], {  stdin: "ok\nerror: disk\nerror: net\n",});console.log(count.stdout); // 2await sbx.exec(["bash", "-c", 'test -n "$DEPLOY_TOKEN"'], {  env: { DEPLOY_TOKEN: process.env.DEPLOY_TOKEN ?? "" },});

The first call passes the pattern as $1, so it is never parsed as shell code. For an API key the code should use but never read, store it as a secret the sandbox never sees.

Keep a shell session between commands

Each exec starts a fresh bash -c, so an export does not carry over to the next one. The code interpreter's Bash keeps its state from one run to the next:

TypeScriptimport { Sandbox } from "withruntime";await using sbx = await Sandbox.create({ network: { internet: false } });await sbx.interpreter.run("export STAGE=build", { language: "bash" });const cell = await sbx.interpreter.run('echo "stage: $STAGE"', { language: "bash" });console.log(cell.stdout); // stage: build
Pythonfrom withruntime import Sandboxwith Sandbox.create(network={"internet": False}) as sbx:    sbx.interpreter.run("export STAGE=build", language="bash")    cell = sbx.interpreter.run('echo "stage: $STAGE"', language="bash")    print(cell["stdout"])

For a real terminal with colours and prompts, sbx.terminal() or npx withruntime sandbox shell <id> opens one. See the code interpreter for how sessions work.

Lint scripts before running them

ShellCheck is an Ubuntu package (shellcheck 0.9.0 on noble, 25 September 2026). Lint the script first and give the warnings to the model that wrote it:

Pythonfrom withruntime import Sandboxwith Sandbox.create(timeout_seconds=600, on_lease_end="stop") as sbx:    sbx.exec("sudo apt-get update -q && sudo apt-get install -y -q shellcheck", check=True, timeout_ms=300_000)    sbx.network.set(internet=False)    sbx.files.write("/workspace/job.sh", 'for f in $(ls *.txt); do cat $f; done\n')    lint = sbx.exec(["shellcheck", "-f", "gcc", "job.sh"])    print(lint.exit_code, lint.stdout)

To have ShellCheck in every sandbox, build an image with recipe: { apt: ["shellcheck"] } (custom images).

Run the same command from your terminal

Terminalnpx withruntime sandbox run --trial -- bash -c 'uname -a; df -h /workspace'

The first run prints a link to approve in your browser; there is no key to copy. New accounts get 50 free sandbox hours, no card. A 2 vCPU, 4 GiB sandbox costs $0.03125 an hour waiting and $0.08 an hour fully busy (pricing).

See also running untrusted LLM code, a coding agent sandbox and turning off a sandbox's internet.

Sources

Checked 25 September 2026.

Facts on this page were checked on 25 September 2026.