@everruns/bashkit-wasm
v0.14.3
Published
Sandboxed bash interpreter for the browser (WebAssembly). Single-threaded — no SharedArrayBuffer, no COOP/COEP headers.
Maintainers
Readme
@everruns/bashkit-wasm
Sandboxed bash interpreter compiled to WebAssembly, for the browser and any other JavaScript runtime — edge/serverless workers (Cloudflare Workers, Vercel Edge, Deno Deploy), Node, Deno, and Bun.
Unlike a WASI-threads build, this package is single-threaded: it needs no
SharedArrayBuffer and no cross-origin isolation (COOP/COEP) headers.
That makes it a drop-in for any web app — including embedded and third-party
iframe contexts where those headers can't be set — and for the constrained edge
runtimes that can't use threads either.
It's a wasm-bindgen module, so it runs in any JS host but not a
non-JS/WASI wasm runtime (wasmtime, wasmer). For a native Node.js / Bun /
Deno addon (faster, no wasm), use
@everruns/bashkit instead;
reach for this package when a native addon can't load — browsers and edge
runtimes.
Live demo
A full interactive terminal built on this package —
examples/browser.
It's a single index.html on Vite: pnpm install && pnpm start, no build step
and no special headers.
Install
npm install @everruns/bashkit-wasmQuick start
import { initBashkit, Bash } from "@everruns/bashkit-wasm";
// Load the .wasm once before constructing Bash.
await initBashkit();
const bash = new Bash();
const result = bash.executeSync('echo "Hello, browser!" | tr a-z A-Z');
console.log(result.stdout); // HELLO, BROWSER!Plain <script type="module"> (no bundler)
<script type="module">
import { initBashkit, Bash } from "https://esm.sh/@everruns/bashkit-wasm";
await initBashkit();
const bash = new Bash();
document.body.textContent = bash.executeSync("seq 1 5 | paste -sd+ | bc").stdout;
</script>Async custom builtins
Register JS callbacks as bash commands. Async callbacks (e.g. issuing a
fetch / GraphQL request) are awaited by execute() — the async API:
const bash = new Bash({
customBuiltins: {
graphql: async (ctx) => {
const res = await fetch("/graphql", {
method: "POST",
headers: { "content-type": "application/json" },
body: ctx.stdin ?? "{}",
});
return await res.text();
},
},
});
const out = await bash.execute('echo "{ me { id } }" | graphql | jq .data');
console.log(out.stdout);ctx is { name, argv, stdin, env, cwd, fs }. Return the builtin's stdout as a
string (or a Promise<string>); throwing becomes stderr with exit code 1.
ctx.fs is a live handle to the same virtual filesystem the script sees, so
a builtin can read inputs and write outputs that later commands pick up:
const bash = new Bash({
customBuiltins: {
"uppercase-file": (ctx) => {
const text = ctx.fs.readFile(ctx.argv[0]);
ctx.fs.writeFile("/out.txt", text.toUpperCase());
return "done\n";
},
},
});
bash.writeFile("/in.txt", "hello\n");
await bash.execute("uppercase-file /in.txt && cat /out.txt"); // -> HELLOctx.fs has readFile, writeFile, appendFile, exists, mkdir, remove,
and ls — the same surface as the Bash VFS helpers below.
Sync vs async
executeSync(cmd)— for plain bash andjq. Fast, returns anExecResultdirectly. Throws only if the script suspends — an async custom builtin;sleepand background jobs do not suspend on wasm (see Limitations).execute(cmd)— returnsPromise<ExecResult>. Required whenever an async custom builtin may run.
Options
new Bash({
username, hostname, cwd,
env: { KEY: "value" },
maxCommands, maxLoopIterations, maxMemory,
files: { "/config.json": '{"debug":true}' },
customBuiltins: { name: (ctx) => "..." },
});Virtual filesystem
Files created via the helpers are visible to scripts and vice versa:
bash.mkdir("/data");
bash.writeFile("/data/x.txt", "hi\n");
bash.appendFile("/data/x.txt", "there\n");
bash.readFile("/data/x.txt"); // "hi\nthere\n"
bash.exists("/data/x.txt"); // true
bash.ls("/data"); // ["x.txt"]
bash.executeSync("cat /data/x.txt").stdout; // "hi\nthere\n"
bash.remove("/data/x.txt");
// bash.fs() returns the same live handle passed to builtins as ctx.fs
const fs = bash.fs();What's included
Plain bash plus the built-in text tooling (grep, sed, awk, jq, find,
…) and jq. Not included in the browser build: outbound HTTP (curl/wget),
ssh, sqlite, and embedded python — these need sockets, threads, or a host
filesystem the browser sandbox doesn't provide. Bridge to the network through a
custom builtin (see above) so requests go through your app's own fetch.
Limitations
- No wall-clock time.
wasm32-unknown-unknownhas no reliable timer driver, sosleep Nelapses instantly and neither thetimeout Nbuiltin nortimeoutMsis enforced. Runaway scripts are instead bounded bymaxCommands,maxLoopIterations, and the parser fuel budget — awhile trueloop throws a resource-limit error rather than hanging. executeSynccan't run async builtins. The single-threaded event loop can't settle aPromisewithout yielding; an async builtin underexecuteSyncfails fast with a clear message. Useexecute().
Examples
examples/browser— the full interactive terminal shown above, on Vite (no build step, no headers).- Minimal, dependency-free demos in
example/— an interactive terminal and an async-builtin/ctx.fsdemo, served by any static file server. Seeexample/README.md.
Development
# Build the bundle and run the headless integration tests:
bash scripts/build.sh
node --test __test__/bashkit-wasm.test.mjs
# or, from the repo root:
just build-wasmLicense
MIT — part of the Bashkit project.

