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 returnstimedOut: truewith its output so far. - An output cap: at most 64 KiB of stdout and 64 KiB of stderr per result,
with
stdoutTruncatedsaying when some was dropped. For more, redirect to a file and read it withfiles.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: buildPythonfrom 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.
- Ubuntu 24.04 (noble) packages: bash, shellcheck
Facts on this page were checked on 25 September 2026.