A sandboxed bash interpreter for AI Agents, built in Rust. Execute bash scripts safely with a virtual filesystem — no containers, no VMs, no host access.
Interactive showcase with 80+ commands running via WASM. Includes an AI agent you can talk to. See examples/website/ for the source.
- Virtual filesystem — all file operations happen in memory by default. No host files are touched.
- 80 commands — echo, cat, grep, awk, sed, jq, find, sort, diff, curl, and many more.
- Full bash syntax — pipelines, redirections, variables, control flow, functions, command substitution, globs, brace expansion, arithmetic, here-documents, case statements.
- Execution limits — 10 configurable bounds (time, commands, loops, output size, call depth, string length, glob results, substitution depth, heredoc size, brace expansion).
- Network policy — sandboxed
curlwith URL allow-lists, method restrictions, redirect and response-size limits. - Multiple filesystem backends — InMemoryFs (default), OverlayFs (copy-on-write), ReadWriteFs (passthrough), MountableFs (composite).
- npm package —
rust-bashwith TypeScript types, native Node.js addon, and WASM support. - AI tool integration — framework-agnostic JSON Schema tool definitions for OpenAI, Anthropic, Vercel AI SDK, LangChain.js.
- MCP server — built-in Model Context Protocol server for Claude Desktop, Cursor, VS Code.
- Embeddable — use as a Rust crate with a builder API. Custom commands via the
VirtualCommandtrait. - CLI binary — standalone
rust-bashcommand with-c,--files,--env,--cwd,--jsonflags, MCP server mode, and an interactive REPL.
npm install rust-bashcargo add rust-bashgit clone https://github.com/shantanugoel/rust-bash.git
cd rust-bash
cargo build --release
# Binary is at target/release/rust-bashcargo install rust-bashimport { Bash, tryLoadNative, createNativeBackend, initWasm, createWasmBackend } from 'rust-bash';
// Auto-detect backend: native addon (fast) or WASM (universal)
let createBackend;
if (await tryLoadNative()) {
createBackend = createNativeBackend;
} else {
await initWasm();
createBackend = createWasmBackend;
}
const bash = await Bash.create(createBackend, {
files: {
'/data.json': '{"name": "world"}',
'/script.sh': 'echo "Hello, $(jq -r .name /data.json)!"',
},
env: { USER: 'agent' },
});
const result = await bash.exec('bash /script.sh');
console.log(result.stdout); // "Hello, world!\n"
console.log(result.exitCode); // 0use rust_bash::RustBashBuilder;
use std::collections::HashMap;
let mut shell = RustBashBuilder::new()
.files(HashMap::from([
("/data.txt".into(), b"hello world".to_vec()),
]))
.env(HashMap::from([
("USER".into(), "agent".into()),
]))
.build()
.unwrap();
let result = shell.exec("cat /data.txt | grep hello").unwrap();
assert_eq!(result.stdout, "hello world\n");
assert_eq!(result.exit_code, 0);import { Bash, defineCommand } from 'rust-bash';
const fetch = defineCommand('fetch', async (args, ctx) => {
const url = args[0];
const response = await globalThis.fetch(url);
return { stdout: await response.text(), stderr: '', exitCode: 0 };
});
const bash = await Bash.create(createBackend, {
customCommands: [fetch],
});
await bash.exec('fetch https://api.example.com/data');use rust_bash::{RustBashBuilder, VirtualCommand, CommandContext, CommandResult};
struct MyCommand;
impl VirtualCommand for MyCommand {
fn name(&self) -> &str { "my-cmd" }
fn execute(&self, args: &[String], ctx: &CommandContext) -> CommandResult {
CommandResult {
stdout: format!("got {} args\n", args.len()),
..Default::default()
}
}
}
let mut shell = RustBashBuilder::new()
.command(Box::new(MyCommand))
.build()
.unwrap();
let result = shell.exec("my-cmd foo bar").unwrap();
assert_eq!(result.stdout, "got 2 args\n");rust-bash exports framework-agnostic tool primitives — use with any AI agent framework:
import {
bashToolDefinition,
createBashToolHandler,
formatToolForProvider,
createNativeBackend,
} from 'rust-bash';
const { handler } = createBashToolHandler(createNativeBackend, {
files: { '/data.txt': 'hello world' },
maxOutputLength: 10000,
});
// Format for your provider
const openaiTool = formatToolForProvider(bashToolDefinition, 'openai');
const anthropicTool = formatToolForProvider(bashToolDefinition, 'anthropic');
// Handle tool calls from the LLM
const result = await handler({ command: 'grep hello /data.txt' });
// { stdout: 'hello world\n', stderr: '', exitCode: 0 }import OpenAI from 'openai';
import { createBashToolHandler, formatToolForProvider, bashToolDefinition, createNativeBackend } from 'rust-bash';
const { handler } = createBashToolHandler(createNativeBackend, { files: myFiles });
const openai = new OpenAI();
const response = await openai.chat.completions.create({
model: 'gpt-4o',
tools: [formatToolForProvider(bashToolDefinition, 'openai')],
messages: [{ role: 'user', content: 'Count lines in /data.txt' }],
});
for (const toolCall of response.choices[0].message.tool_calls ?? []) {
const result = await handler(JSON.parse(toolCall.function.arguments));
}import Anthropic from '@anthropic-ai/sdk';
import { createBashToolHandler, formatToolForProvider, bashToolDefinition, createNativeBackend } from 'rust-bash';
const { handler } = createBashToolHandler(createNativeBackend, { files: myFiles });
const anthropic = new Anthropic();
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
tools: [formatToolForProvider(bashToolDefinition, 'anthropic')],
messages: [{ role: 'user', content: 'Count lines in /data.txt' }],
});
for (const block of response.content) {
if (block.type === 'tool_use') {
const result = await handler(block.input);
}
}import { tool } from 'ai';
import { z } from 'zod';
import { createBashToolHandler, createNativeBackend } from 'rust-bash';
const { handler } = createBashToolHandler(createNativeBackend, { files: myFiles });
const bashTool = tool({
description: 'Execute bash commands in a sandbox',
parameters: z.object({ command: z.string() }),
execute: async ({ command }) => handler({ command }),
});import { tool } from '@langchain/core/tools';
import { z } from 'zod';
import { createBashToolHandler, createNativeBackend } from 'rust-bash';
const { handler, definition } = createBashToolHandler(createNativeBackend, { files: myFiles });
const bashTool = tool(
async ({ command }) => JSON.stringify(await handler({ command })),
{ name: definition.name, description: definition.description, schema: z.object({ command: z.string() }) },
);See AI Agent Tool Recipe for complete agent loop examples.
The CLI binary includes a built-in Model Context Protocol server:
rust-bash --mcpExposed tools: bash, write_file, read_file, list_directory. State persists across calls.
{
"mcpServers": {
"rust-bash": {
"command": "rust-bash",
"args": ["--mcp"]
}
}
}{
"servers": {
"rust-bash": {
"type": "stdio",
"command": "rust-bash",
"args": ["--mcp"]
}
}
}See MCP Server Setup for Cursor, Windsurf, Cline, and other clients.
import { Bash, initWasm, createWasmBackend } from 'rust-bash/browser';
await initWasm();
const bash = await Bash.create(createWasmBackend, {
files: { '/hello.txt': 'Hello from WASM!' },
});
const result = await bash.exec('cat /hello.txt');
console.log(result.stdout); // "Hello from WASM!\n"| Feature | just-bash | rust-bash |
|---|---|---|
| Language | Pure TypeScript | Rust → WASM + native addon |
| Performance | JS-speed | Near-native (native addon) / WASM |
| API | new Bash(opts) |
Bash.create(backend, opts) |
| Custom commands | defineCommand() |
defineCommand() (same API) |
| AI integration | Vercel AI SDK only | Framework-agnostic (OpenAI, Anthropic, Vercel, LangChain) |
| MCP server | ❌ | ✅ Built-in (rust-bash --mcp) |
| Browser | ✅ | ✅ (WASM) |
| Node.js native | ❌ | ✅ (napi-rs) |
| C FFI | ❌ | ✅ (shared library) |
| Filesystem backends | In-memory only | InMemoryFs, OverlayFs, ReadWriteFs, MountableFs |
| Execution limits | ✅ | ✅ (10 configurable bounds) |
| Network policy | ❌ | ✅ (URL allow-list, method restrictions) |
# Execute a command
rust-bash -c 'echo hello | wc -c'
# Seed files from host disk into the virtual filesystem
rust-bash --files /path/to/data.txt:/data.txt -c 'cat /data.txt'
rust-bash --files /path/to/dir -c 'ls /'
# Set environment variables
rust-bash --env USER=agent --env HOME=/home/agent -c 'echo $USER'
# Set working directory
rust-bash --cwd /app -c 'pwd'
# JSON output for machine consumption
rust-bash --json -c 'echo hello'
# {"stdout":"hello\n","stderr":"","exit_code":0}
# Execute a script file with positional arguments
rust-bash script.sh arg1 arg2
# Read commands from stdin
echo 'echo hello' | rust-bash
# MCP server mode
rust-bash --mcp
# Interactive REPL (starts when no command/script/stdin is given)
rust-bashWhen launched without -c, a script file, or piped stdin, rust-bash starts an
interactive REPL with readline support:
- Colored prompt —
rust-bash:{cwd}$reflecting the current directory, green (exit 0) or red (non-zero last exit) - Tab completion — completes built-in command names
- Multi-line input — incomplete constructs (e.g.,
if true; then) wait for more input - History — persists across sessions in
~/.rust_bash_history - Ctrl-C — cancels the current input line
- Ctrl-D — exits the REPL with the last command's exit code
exit [N]— exits with code N (default 0)
- AI agent tools — give LLMs a bash sandbox without container overhead
- Code sandboxes — run user-submitted scripts safely
- Testing — deterministic bash execution with a controlled filesystem
- Embedded scripting — add bash scripting to Rust applications
- MCP server — provide bash execution to Claude Desktop, Cursor, VS Code
| Category | Commands |
|---|---|
| Core | echo, cat, true, false, pwd, touch, mkdir, ls, test, [ |
| File ops | cp, mv, rm, tee, stat, chmod, ln, readlink, rmdir, du, split |
| Text | grep, egrep, fgrep, sort, uniq, cut, head, tail, wc, tr, rev, fold, nl, printf, paste, od, tac, comm, join, fmt, column, expand, unexpand, strings |
| Text processing | sed, awk, jq, diff |
| Search | rg |
| Navigation | realpath, basename, dirname, tree, find |
| Utilities | expr, date, sleep, seq, env, printenv, which, base64, md5sum, sha1sum, sha256sum, whoami, hostname, uname, yes, xargs, timeout, file, bc, clear |
| Compression | gzip, gunzip, zcat, tar |
| Network | curl (feature-gated) |
All commands support --help for built-in usage information.
exit, cd, export, unset, set, shift, readonly, declare, read, eval, source / ., break, continue, : / colon, let, local, return, trap, shopt, type, command, builtin, getopts, mapfile / readarray, pushd, popd, dirs, hash, wait, alias, unalias, printf, exec, sh / bash, help, history
Additionally, if/then/elif/else/fi, for/while/until/do/done, case/esac, ((...)), [[ ]], and time are handled as shell syntax by the interpreter.
use rust_bash::{RustBashBuilder, ExecutionLimits, NetworkPolicy};
use std::collections::HashMap;
use std::time::Duration;
let mut shell = RustBashBuilder::new()
.files(HashMap::from([
("/app/script.sh".into(), b"echo hello".to_vec()),
]))
.env(HashMap::from([
("HOME".into(), "/home/agent".into()),
]))
.cwd("/app")
.execution_limits(ExecutionLimits {
max_command_count: 1_000,
max_execution_time: Duration::from_secs(5),
..Default::default()
})
.network_policy(NetworkPolicy {
enabled: true,
allowed_url_prefixes: vec!["https://api.example.com/".into()],
..Default::default()
})
.build()
.unwrap();Tip: To allow all URLs, set
allowed_url_prefixestovec!["http://".into(), "https://".into()]. Wildcards are not supported — the policy uses prefix matching.
| Limit | Default |
|---|---|
max_call_depth |
25 |
max_command_count |
10,000 |
max_loop_iterations |
10,000 |
max_execution_time |
30 s |
max_output_size |
10 MB |
max_string_length |
10 MB |
max_glob_results |
100,000 |
max_substitution_depth |
50 |
max_heredoc_size |
10 MB |
max_brace_expansion |
10,000 |
| Backend | Description |
|---|---|
InMemoryFs |
Default. All data in memory. Zero host access. |
OverlayFs |
Copy-on-write over a real directory. Reads from disk, writes stay in memory. |
ReadWriteFs |
Passthrough to real filesystem. For trusted execution. |
MountableFs |
Compose backends at different mount points. |
use rust_bash::{RustBashBuilder, OverlayFs};
use std::sync::Arc;
// Reads from ./my_project on disk; writes stay in memory
let overlay = OverlayFs::new("./my_project").unwrap();
let mut shell = RustBashBuilder::new()
.fs(Arc::new(overlay))
.cwd("/")
.build()
.unwrap();
let result = shell.exec("cat /src/main.rs").unwrap(); // reads from disk
shell.exec("echo patched > /src/main.rs").unwrap(); // writes to memory onlyuse rust_bash::{RustBashBuilder, ReadWriteFs};
use std::sync::Arc;
// Restricted to /tmp/sandbox (chroot-like)
let rwfs = ReadWriteFs::with_root("/tmp/sandbox").unwrap();
let mut shell = RustBashBuilder::new()
.fs(Arc::new(rwfs))
.cwd("/")
.build()
.unwrap();
shell.exec("echo hello > /output.txt").unwrap(); // writes to /tmp/sandbox/output.txtuse rust_bash::{RustBashBuilder, InMemoryFs, MountableFs, OverlayFs};
use std::sync::Arc;
let mountable = MountableFs::new()
.mount("/", Arc::new(InMemoryFs::new())) // in-memory root
.mount("/project", Arc::new(OverlayFs::new("./myproject").unwrap())) // overlay on real project
.mount("/tmp", Arc::new(InMemoryFs::new())); // separate temp space
let mut shell = RustBashBuilder::new()
.fs(Arc::new(mountable))
.cwd("/")
.build()
.unwrap();
shell.exec("cat /project/README.md").unwrap(); // reads from disk
shell.exec("echo scratch > /tmp/work").unwrap(); // writes to in-memory /tmprust-bash can be used from any language with C FFI support (Python, Go, Ruby, etc.) via a shared library.
cargo build --features ffi --release
# Output: target/release/librust_bash.so (Linux), .dylib (macOS), .dll (Windows)
# Header: include/rust_bash.h#include "rust_bash.h"
#include <stdio.h>
int main(void) {
struct RustBash *sb = rust_bash_create(NULL);
struct ExecResult *r = rust_bash_exec(sb, "echo hello world");
printf("%.*s", r->stdout_len, r->stdout_ptr);
rust_bash_result_free(r);
rust_bash_free(sb);
return 0;
}For complete Python and Go examples, see examples/ffi/. For the full FFI guide, see the FFI Usage recipe.
| Type | Description |
|---|---|
RustBashBuilder |
Builder for configuring and constructing a shell instance |
RustBash |
The shell instance — call .exec(script) to run commands |
ExecResult |
Returned by exec(): stdout, stderr, exit_code |
ExecutionLimits |
Configurable resource bounds |
NetworkPolicy |
URL allow-list and HTTP method restrictions for curl |
VirtualCommand |
Trait for registering custom commands |
CommandContext |
Passed to command implementations (fs, cwd, env, stdin, limits) |
CommandResult |
Returned by command implementations |
RustBashError |
Top-level error: Parse, Execution, LimitExceeded, Network, Vfs, Timeout |
VfsError |
Filesystem errors: NotFound, AlreadyExists, PermissionDenied, etc. |
Variable |
A shell variable with value, exported, readonly metadata |
ShellOpts |
Shell option flags: errexit, nounset, pipefail, xtrace |
ExecutionCounters |
Per-exec() resource usage counters |
InterpreterState |
Full mutable shell state (advanced: direct inspection/manipulation) |
ExecCallback |
Callback type for sub-command execution (xargs, find -exec) |
InMemoryFs |
In-memory filesystem backend |
OverlayFs |
Copy-on-write overlay backend |
ReadWriteFs |
Real filesystem passthrough backend |
MountableFs |
Composite backend with path-based mount delegation |
VirtualFs |
Trait for filesystem backends |
- Guidebook — architecture, design, and implementation details
- Recipes — task-oriented guides for common use cases
- npm package README — TypeScript API reference
- AI Agent Guide — quick-start guide for AI agents consuming rust-bash
- Planned: Embedded runtimes — SQLite, yq, Python, JavaScript
- Planned: Platform features — cancellation, lazy files, AST transforms, fuzz testing
MIT