A simulated bash environment with an in-memory virtual filesystem, written in TypeScript.
Designed for AI agents that need a secure, sandboxed bash environment.
Supports optional network access via curl with secure-by-default URL filtering.
Note: This is pre-released alpha software. Use at your own risk and please provide feedback.
- Security model
- Installation
- Usage
- Supported Commands
- Shell Features
- Default Layout
- Network Access
- Execution Protection
- Development
- The shell only has access to the provided file system.
- Execution is protected against infinite loops or recursion through. However, Bash is not fully robust against DOS from input. If you need to be robust against this, use process isolation at the OS level.
- Binaries or even WASM are inherently unsupported (Use Vercel Sandbox or a similar product if a full VM is needed).
- There is no network access by default.
- Network access can be enabled, but requests are checked against URL prefix allow-lists and HTTP-method allow-lists. See network access for details
npm install just-bashimport { Bash } from "just-bash";
const env = new Bash();
await env.exec('echo "Hello" > greeting.txt');
const result = await env.exec("cat greeting.txt");
console.log(result.stdout); // "Hello\n"
console.log(result.exitCode); // 0
console.log(result.env); // Final environment after executionEach exec() is isolated—env vars, functions, and cwd don't persist across calls (filesystem does).
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"
});
// Per-exec overrides
await env.exec("echo $TEMP", { env: { TEMP: "value" }, cwd: "/tmp" });Extend just-bash with your own TypeScript commands using defineCommand:
import { Bash, 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) => {
return { stdout: 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 commands receive the full CommandContext with access to fs, cwd, env, stdin, and exec for running subcommands.
Three filesystem implementations are available:
InMemoryFs (default) - Pure in-memory filesystem, no disk access:
import { Bash } from "just-bash";
const env = new Bash(); // Uses InMemoryFs by defaultOverlayFs - Copy-on-write over a real directory. Reads come from disk, writes stay in memory:
import { Bash } from "just-bash";
import { OverlayFs } from "just-bash/fs/overlay-fs";
const overlay = new OverlayFs({ root: "/path/to/project" });
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 memoryReadWriteFs - Direct read-write access to a real directory. Use this if you want the agent to be agle to write to your disk:
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 filesystemCreates a bash tool for use with the AI SDK, because agents love bash.
import { createBashTool } from "just-bash/ai";
import { generateText } from "ai";
const bashTool = createBashTool({
files: { "/data/users.json": '[{"name": "Alice"}, {"name": "Bob"}]' },
});
const result = await generateText({
model: "anthropic/claude-haiku-4.5",
tools: { bash: bashTool },
prompt: "Count the users in /data/users.json",
});See examples/bash-agent for a full implementation.
Bash provides a Sandbox class that's API-compatible with @vercel/sandbox, making it easy to swap implementations. You can start with Bash and switch to a real sandbox when you need the power of a full VM (e.g. to run node, python, or custom binaries).
import { Sandbox } from "just-bash";
// Create a sandbox instance
const sandbox = await Sandbox.create({ cwd: "/app" });
// Write files to the virtual filesystem
await sandbox.writeFiles({
"/app/script.sh": 'echo "Hello World"',
"/app/data.json": '{"key": "value"}',
});
// Run commands and get results
const cmd = await sandbox.runCommand("bash /app/script.sh");
const output = await cmd.stdout(); // "Hello World\n"
const exitCode = (await cmd.wait()).exitCode; // 0
// Read files back
const content = await sandbox.readFile("/app/data.json");
// Create directories
await sandbox.mkDir("/app/logs", { recursive: true });
// Clean up (no-op for Bash, but API-compatible)
await sandbox.stop();After installing globally (npm install -g just-bash), use the just-bash command as a secure alternative to bash for AI agents:
# Execute inline script
just-bash -c 'ls -la && cat package.json | head -5'
# Execute with specific project root
just-bash -c 'grep -r "TODO" src/' --root /path/to/project
# Pipe script from stdin
echo 'find . -name "*.ts" | wc -l' | just-bash
# Execute a script file
just-bash ./scripts/deploy.sh
# Get JSON output for programmatic use
just-bash -c 'echo hello' --json
# Output: {"stdout":"hello\n","stderr":"","exitCode":0}The CLI uses OverlayFS - reads come from the real filesystem, but all writes stay in memory and are discarded after execution. The project root is mounted at /home/user/project.
Options:
-c <script>- Execute script from argument--root <path>- Root directory (default: current directory)--cwd <path>- Working directory in sandbox-e, --errexit- Exit on first error--json- Output as JSON
pnpm shellThe interactive shell has full internet access enabled by default, allowing you to use curl to fetch data from any URL. Use --no-network to disable this:
pnpm shell --no-networkcat, cp, ln, ls, mkdir, mv, readlink, rm, stat, touch, tree
awk, base64, cut, diff, grep, head, jq, printf, sed, sort, tail, tr, uniq, wc, xargs
basename, cd, dirname, du, echo, env, export, find, printenv, pwd, tee
alias, bash, chmod, clear, date, expr, false, help, history, seq, sh, sleep, timeout, true, unalias, which
curl, html-to-markdown
All commands support --help for usage information.
- 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 { ... }orname() { ... } - Local variables:
local VAR=value - Loops:
for,while,until - Symbolic links:
ln -s target link - Hard links:
ln target link
When created without options, Bash provides a Unix-like directory structure:
/home/user- Default working directory (and$HOME)/bin- Contains stubs for all built-in commands/usr/bin- Additional binary directory/tmp- Temporary files directory
Commands can be invoked by path (e.g., /bin/ls) or by name.
Network access (and the curl command) is disabled by default for security. To enable it, configure the network option:
// Allow specific URLs with GET/HEAD only (safest)
const env = new Bash({
network: {
allowedUrlPrefixes: [
"https://api.github.com/repos/myorg/",
"https://api.example.com",
],
},
});
// Allow specific URLs with additional methods
const env = new Bash({
network: {
allowedUrlPrefixes: ["https://api.example.com"],
allowedMethods: ["GET", "HEAD", "POST"], // Default: ["GET", "HEAD"]
},
});
// Allow all URLs and methods (use with caution)
const env = new Bash({
network: { dangerouslyAllowFullInternetAccess: true },
});Note: The curl command only exists when network is configured. Without network configuration, curl returns "command not found".
The allow-list enforces:
- Origin matching: URLs must match the exact origin (scheme + host + port)
- Path prefix: Only paths starting with the specified prefix are allowed
- HTTP method restrictions: Only GET and HEAD by default (configure
allowedMethodsfor more) - Redirect protection: Redirects to non-allowed URLs are blocked
# Fetch and process data
curl -s https://api.example.com/data | grep pattern
# Download and convert HTML to Markdown
curl -s https://example.com | html-to-markdown
# POST JSON data
curl -X POST -H "Content-Type: application/json" \
-d '{"key":"value"}' https://api.example.com/endpointBash protects against infinite loops and deep recursion with configurable limits:
const env = new Bash({
executionLimits: {
maxCallDepth: 100, // Max function recursion depth
maxCommandCount: 10000, // Max total commands executed
maxLoopIterations: 10000, // Max iterations per loop
maxAwkIterations: 10000, // Max iterations in awk programs
maxSedIterations: 10000, // Max iterations in sed scripts
},
});All limits have sensible defaults. Error messages include hints on which limit to increase. Feel free to increase if your scripts intentionally go beyond them.
pnpm test # Run tests in watch mode
pnpm test:run # Run tests once
pnpm typecheck # Type check without emitting
pnpm build # Build TypeScript
pnpm shell # Run interactive shellApache-2.0