# 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](/docs/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](/docs/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](/docs/javascript#run-commands)). The image has bash, `git`, `curl`, `wget`, `jq`, `rg`, `fd`, `sqlite3`, `zip`, `unzip`, `xz`, `gcc` and `make` ([the sandbox environment](/docs/sandbox-environment#what-is-installed)). 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. ```ts import { Sandbox } from "withruntime"; const script = `set -euo pipefail mkdir -p out for n in 1 2 3; do echo "item $n" >> out/list.txt; done wc -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); ``` ```python from withruntime import Sandbox script = """set -euo pipefail mkdir -p out for n in 1 2 3; do echo "item $n" >> out/list.txt; done wc -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. ```ts import { 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); // 2 await 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](/docs/security#secrets-sandboxes-never-see). ## 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: ```ts check import { 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 ``` ```python check from withruntime import Sandbox with 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 ` opens one. See the [code interpreter](/glossary/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: ```python check from withruntime import Sandbox with 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](/docs/images)). ## Run the same command from your terminal ```bash no-run npx 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](/docs/pricing)). See also [running untrusted LLM code](/use-cases/run-untrusted-llm-code), [a coding agent sandbox](/use-cases/coding-agent-sandbox) and [turning off a sandbox's internet](/how-to/turn-off-sandbox-internet). ## Sources Checked 25 September 2026. - Ubuntu 24.04 (noble) packages: [bash](https://packages.ubuntu.com/noble/bash), [shellcheck](https://packages.ubuntu.com/noble/shellcheck) Facts on this page were checked on 25 September 2026.