Skip to content
Runtime

How to run Bash scripts and shell commands in a sandbox

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

On Runtime, every command runs in bash inside its own microVM. Nothing needs to be installed. A string sent to exec runs under bash -c as the user runtime, with passwordless sudo. The sandbox is a Firecracker microVM that runs Ubuntu 24.04.5. Root in the sandbox still cannot change the sandbox's network, CPU, memory or cost (security).

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 (Ubuntu's archive, 26 October 2026).

Run a script an agent wrote

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

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)

Each result returns the following, and these settings protect you:

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

Pass secrets and input safely

Put a secret in env, never in the command text. Runtime never echoes it 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 the pattern is never parsed as shell code. For an API key that the code should use but never read, store the key as a secret the sandbox never sees.

Keep a shell session between commands

Each exec starts a new bash -c, so an export does not carry over to the next call. 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 colors and prompts, run sbx.terminal() or npx withruntime sandboxes shell <id>. See the code interpreter for how sessions work.

Lint scripts before running them

ShellCheck is an Ubuntu package (shellcheck 0.9.0 on noble, 26 October 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 sandboxes run -- bash -c 'uname -a; df -h /workspace'

The first run prints a link to approve in your browser. There is no key to copy. A new account gets 100 free hours of a 1 GB machine in its first 30 days, with no card required. A 2 vCPU, 4 GiB sandbox costs $0.03125 an hour while it waits, and $0.08 an hour when fully busy (pricing).

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

Sources

Checked 26 October 2026.

Facts on this page were checked on 26 October 2026.

Related.

100 free hoursin your first 30 days.

of a 1 GB machine

  • No credit card
  • Four sandboxes at once, 2 vCPU and 4 GiB each
  • Then prepaid credit from $10, no plan fee
Start free, no card
Start free, no card