Qortora · Search · Indexed page

justbash.devFetched 2026-08-17T11:45:10Z

just-bash

A sandboxed bash interpreter for AI agents. Pure TypeScript with in-memory filesystem.

Open original source · Full cached text

just-bash just-bash v3.3.0 A TypeScript bash interpreter with in-memory filesystem. Designed for AI agents needing a secure, sandboxed environment. Custom commands: about About just-bash install Installation instructions github GitHub repository Or try any bash command: ls, cat, echo, grep, awk, jq, sed, etc. Type 'help' for a list of all built-in commands. npm install just-bash Usage: import { Bash } from "just-bash"; const bash = new Bash(); const result = await bash.exec("echo hello"); https://github.com/vercel-labs/just-bash # just-bash A virtual bash environment with an in-memory filesystem, written in TypeScript and designed for AI agents. Broad support for standard unix commands and bash syntax with optional curl, Python, JS/TS, and sqlite support. **Note**: This is beta software. Use at your own risk and please provide feedback. See [security model](#security-model). ## Quick Start ```bash npm install just-bash ``` ```typescript import { Bash } from "just-bash"; const bash = new Bash(); await bash.exec('echo "Hello" > greeting.txt'); const result = await bash.exec("cat greeting.txt"); console.log(result.stdout); // "Hello\n" console.log(result.exitCode); // 0 ``` Each `exec()` call gets its own isolated shell state — environment variables, functions, and working directory reset between calls. The **filesystem is shared** across calls, so files written in one `exec()` are visible in the next. ## Custom Commands Extend just-bash with your own TypeScript commands using `defineCommand`: ```typescript import { Bash, decodeBytesToUtf8, defineCommand } from "just-bash"; const hello = defineCommand("hello", async (args, ctx) => { const name = args[0] || "world"; return { stdout: `Hello, ${name}!\n`, stderr: "", exitCode: 0 }; }); const upper = defineCommand("upper", async (args, ctx) => { // ctx.stdin is a ByteString — decode to text before string ops. return { stdout: decodeBytesToUtf8(ctx.stdin).toUpperCase(), stderr: "", exitCode: 0, }; }); const bash = new Bash({ customCommands: [hello, upper] }); await bash.exec("hello Alice"); // "Hello, Alice!\n" await bash.exec("echo 'test' | upper"); // "TEST\n" ``` Custom command callbacks receive a `ResolvedCommandContext` with `fs`, `cwd`, `env`, `stdin`, resolved `limits`, and `exec` (for subcommands), and work with pipes, redirections, and all shell features. The legacy `CommandContext` remains available for standalone context inputs; use `createCommandContext({ fs })` when calling a command directly with a fully resolved context. Host-provided commands preserve the legacy trusted default whether supplied to the `Bash` constructor, declared through `defineCommand`, loaded lazily, or added later with `bash.registerCommand()`. Set `trusted: false` (or use `defineCommand(name, execute, { trusted: false })`) to select the restricted extension boundary. Trusted commands run in the embedding process and should never execute guest-provided JavaScript. Every invocation is bound by `maxExecutionTimeMs`. On cancellation, just-bash revokes the command context immediately; `maxExtensionCleanupTimeMs` only bounds how long it waits for the now-authority-free command promise to settle. A late continuation cannot use `ctx.fs`, `ctx.env`, `ctx.exec`, or other context capabilities. Cleanup work that must run at scope closure can be registered with `ctx.executionScope.registerCleanup()`. A cleanup failure is returned as a generic exit-126 shell result rather than rejecting `Bash.exec()` or exposing host error details. JavaScript cannot forcibly stop arbitrary host code, so extensions requiring a hard guarantee against external side effects must run in a terminable worker or process. Tests that invoke command objects directly can use `createCommandContext({ fs })` to get a fully resolved context without duplicating internal defaults. <details> <summary><h2>Supported Commands</h2></summary> ### File Operations `cat`, `cp`, `file`, `ln`, `ls`, `mkdir`, `mv`, `readlink`, `rm`, `rmdir`, `split`, `stat`, `touch`, `tree` ### Text Processing `awk`, `base64`, `column`, `comm`, `cut`, `diff`, `expand`, `fold`, `grep` (+ `egrep`, `fgrep`), `head`, `join`, `md5sum`, `nl`, `od`, `paste`, `printf`, `rev`, `rg`, `sed`, `sha1sum`, `sha256sum`, `sort`, `strings`, `tac`, `tail`, `tr`, `unexpand`, `uniq`, `wc`, `xargs` ### Data Processing `jq` (JSON), `sqlite3` (SQLite), `xan` (CSV), `yq` (YAML/XML/TOML/CSV) ### Optional Runtimes `js-exec` (JavaScript/TypeScript via QuickJS; requires `javascript: true`), `python3`/`python` (Python via CPython; requires `python: true`) ### Compression & Archives `gzip` (+ `gunzip`, `zcat`), `tar` ### Navigation & Environment `basename`, `cd`, `dirname`, `du`, `echo`, `env`, `export`, `find`, `hostname`, `printenv`, `pwd`, `tee` ### Shell Utilities `alias`, `bash`, `chmod`, `clear`, `date`, `expr`, `false`, `help`, `history`, `seq`, `sh`, `sleep`, `time`, `timeout`, `true`, `unalias`, `which`, `whoami` ### Network `curl`, `html-to-markdown` (require [network configuration](#network-access)) All commands support `--help` for usage information. ### Shell Features - **Pipes**: `cmd1 | cmd2` - **Redirections**: `>`, `>>`, `2>`, `2>&1`, `<` - **Command chaining**: `&&`, `||`, `;` - **Variables**: `$VAR`, `${VAR}`, `${VAR:-default}` - **Positional parameters**: `$1`, `$2`, `$@`, `$#` - **Glob patterns**: `*`, `?`, `[...]` - **If statements**: `if COND; then CMD; elif COND; then CMD; else CMD; fi` - **Functions**: `function name { ... }` or `name() { ... }` - **Local variables**: `local VAR=value` - **Loops**: `for`, `while`, `until` - **Symbolic links**: `ln -s target link` - **Hard links**: `ln target link` </details> ## Configuration ```typescript const env = new Bash({ files: { "/data/file.txt": "content" }, // Initial files env: { MY_VAR: "value" }, // Initial environment cwd: "/app", // Starting directory (default: /home/user) executionLimits: { maxCallDepth: 50 }, // See "Execution Protection" python: true, // Enable python3/python commands javascript: true, // Enable js-exec command // Or with bootstrap: javascript: { bootstrap: "globalThis.X = 1;" } }); // Per-exec overrides await env.exec("echo $TEMP", { env: { TEMP: "value" }, cwd: "/tmp" }); // Pass stdin to the script await env.exec("cat", { stdin: "hello from stdin\n" }); // Start with a clean environment await env.exec("env", { replaceEnv: true, env: { ONLY: "this" } }); // Pass arguments without shell escaping (like spawnSync) await env.exec("grep", { args: ["-r", "TODO", "src/"] }); // Cancel long-running scripts const controller = new AbortController(); setTimeout(() => controller.abort(), 5000); await env.exec("while true; do sleep 1; done", { signal: controller.signal }); // Preserve leading whitespace (e.g., for heredocs) await env.exec("cat <<EOF\n indented\nEOF", { rawScript: true }); ``` ### Timezone `date` defaults to UTC (`%Z=UTC`, `%z=+0000`) regardless of the host clock, so the sandbox does not leak the host timezone. To opt into a specific zone, pass `TZ` as an initial env var: ```typescript const bash = new Bash({ env: { TZ: "America/New_York" } }); await bash.exec("date"); // Mon Jun 1 09:30:00 EDT 2026 ``` `-u` always forces UTC; an unset or invalid `$TZ` falls back to UTC. Setting `TZ` exposes that timezone to scripts running in the sandbox, so only pass a value you are comfortable revealing — forwarding the host's real `$TZ` (e.g. `process.env.TZ`) reintroduces the disclosure that the UTC default exists to prevent. `exec()` options: | Option | Type | Description | |---|---|---| | `env` | `Record<string, string>` | Environment variables for this execution only | | `cwd` | `string` | Working directory for this execution only | | `stdin` | `string` | Standard input passed to the script | | `args` | `string[]` | Additional argv passed directly to the first command (bypasses shell parsing; does not change `$1`, `$2`, ...) | | `replaceEnv` | `boolean` | Start with empty env instead of merging (default: `false`) | | `signal` | `AbortSignal` | Cooperative cancellation; stops at next statement boundary | | `rawScript` | `boolean` | Skip leading-whitespace normalization (default: `false`) | ## Filesystem Options Four filesystem implementations: **InMemoryFs** (default) - Pure in-memory filesystem, no disk access: ```typescript import { Bash } from "just-bash"; const env = new Bash({ files: { "/data/config.json": '{"key": "value"}', // Lazy: called on first read, cached. Never called if written before read. "/data/large.csv": () => "col1,col2\na,b\n", "/data/remote.txt": async () => (await fetch("https://example.com")).text(), }, }); ``` **OverlayFs** - Copy-on-write over a real directory. Reads come from disk, writes stay in memory: ```typescript import { Bash } from "just-bash"; import { OverlayFs } from "just-bash/fs/overlay-fs"; const overlay = new OverlayFs({ root: "/path/to/project", // Copy-on-write data is bounded independently from real-file reads. maxMemoryBytes: 256 * 1024 * 1024, }); const env = new Bash({ fs: overlay, cwd: overlay.getMountPoint() }); await env.exec("cat package.json"); // reads from disk await env.exec('echo "modified" > package.json'); // stays in memory ``` `maxMemoryBytes` defaults to 1 GiB and covers aggregate files retained in the copy-on-write layer, including append chunks. Set it to the deployment's memory budget when an `OverlayFs` is reused across executions. **ReadWriteFs** - Direct read-write access to a real directory. Use this if you want the agent to be able to write to your disk: ```typescript import { Bash } from "just-bash"; import { ReadWriteFs } from "just-bash/fs/read-write-fs"; const rwfs = new ReadWriteFs({ root: "/path/to/sandbox" }); const env = new Bash({ fs: rwfs }); await env.exec('echo "hello" > file.txt'); // writes to real filesystem ``` Keep `ReadWriteFs` pointed at a workspace directory, not at the installed `just-bash` package or any other trusted runtime code. Guest-writable roots should stay separate from trusted code. `ReadWriteFs` uses normal in-place filesystem operations for private regular files. For multiply-linked regular files, it isolates append and metadata changes by copying the file and replacing only the sandbox directory entry, so a host-created hard link cannot carry those changes beyond the configured root. Implicit copies are limited by `maxCopyOnWriteSize` (100 MB by default; set it to `0` to disable the limit). Overwrite does not need to copy existing content. Explicit `cp` copies can be limited with the opt-in `maxCopySize` option (unlimited by default). The portable copy path may materialize sparse-file holes, so embeddings that require a disk-allocation bound should configure `maxCopySize`. Shared-inode isolation has a few deliberate limitations: - Append, `chmod`, and `utimes` on a multiply-linked regular file require read access to the file and write access to its parent directory. They fail with `EFBIG` when the file exceeds `maxCopyOnWriteSize`. - Copies use `O_NOATIME` when the Node.js runtime exposes it and retry with normal read semantics if the kernel returns `EPERM`. Runtimes and platforms without `O_NOATIME` may update access-time metadata visible through another hard link. - Do not mutate a `ReadWriteFs` root concurrently through direct host filesystem APIs. Node.js does not expose the descriptor-relative operations needed to make pathname validation atomic against an external actor. A concurrent host append to a multiply-linked file may be lost when the isolated entry is replaced. - Mutations in overlapping `ReadWriteFs` roots are serialized within the process. Unrelated roots proceed independently. The queue is not cancellable or bounded, so a large mutation can delay later operations in overlapping roots even if the requesting script is subsequently aborted. - Content writes and appends to FIFOs, sockets, devices, and other special files are rejected. This avoids indefinitely occupying an overlapping-root mutation slot on a blocking special-file open. Metad…