rust-bash 0.3.0

A sandboxed bash interpreter for AI Agents with a virtual filesystem
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
# rust-bash

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.

> **Status: alpha / Milestones 1–7 Complete** — Core interpreter, text processing,
> execution safety, filesystem backends, CLI binary, C FFI, WASM target, npm package,
> AI SDK integration, shell language completeness, and command coverage are implemented.

### 🌐 [Try it in the browser →]https://rustbash.dev

Interactive showcase with 80+ commands running via WASM. Includes an AI agent you can talk to. See [`examples/website/`](examples/website/) for the source.

## Highlights

- **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 `curl` with 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-bash` with 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 `VirtualCommand` trait.
- **CLI binary** — standalone `rust-bash` command with `-c`, `--files`, `--env`, `--cwd`, `--json` flags, MCP server mode, and an interactive REPL.

## Installation

### npm (TypeScript / JavaScript)

```bash
npm install rust-bash
```

### Rust crate

```bash
cargo add rust-bash
```

### Build from source (Rust)

```bash
git clone https://github.com/shantanugoel/rust-bash.git
cd rust-bash
cargo build --release
# Binary is at target/release/rust-bash
```

### Install via Cargo

```bash
cargo install rust-bash
```

## Quick Start (TypeScript)

```typescript
import { 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); // 0
```

## Quick Start (Rust)

```rust
use 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);
```

## Custom Commands

### TypeScript

```typescript
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');
```

### Rust

```rust
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");
```

## AI Tool Integration

`rust-bash` exports framework-agnostic tool primitives — use with any AI agent framework:

```typescript
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 }
```

### OpenAI

```typescript
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));
}
```

### Anthropic

```typescript
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);
  }
}
```

### Vercel AI SDK

```typescript
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 }),
});
```

### LangChain.js

```typescript
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](docs/recipes/ai-agent-tool.md) for complete agent loop examples.

## MCP Server

The CLI binary includes a built-in [Model Context Protocol](https://modelcontextprotocol.io/) server:

```bash
rust-bash --mcp
```

Exposed tools: `bash`, `write_file`, `read_file`, `list_directory`. State persists across calls.

### Claude Desktop

```json
{
  "mcpServers": {
    "rust-bash": {
      "command": "rust-bash",
      "args": ["--mcp"]
    }
  }
}
```

### VS Code (GitHub Copilot)

```json
{
  "servers": {
    "rust-bash": {
      "type": "stdio",
      "command": "rust-bash",
      "args": ["--mcp"]
    }
  }
}
```

See [MCP Server Setup](docs/recipes/mcp-server.md) for Cursor, Windsurf, Cline, and other clients.

## Browser / WASM

```typescript
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"
```

## Performance

| 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) |

## CLI Binary

```bash
# 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-bash
```

### Interactive REPL

When 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)

## Use Cases

- **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

## Built-in Commands

### Registered commands (80)

| 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.

### Interpreter builtins (40)

`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.

## Configuration (Rust)

```rust
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();
```

### Execution limits defaults

| Limit | Default |
|-------|---------|
| `max_call_depth` | 100 |
| `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 |

## Filesystem Backends

| 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. |

### OverlayFs — Read real files, sandbox writes

```rust
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 only
```

### ReadWriteFs — Direct filesystem access

```rust
use 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.txt
```

### MountableFs — Combine backends at mount points

```rust
use 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 /tmp
```

## C FFI

rust-bash can be used from any language with C FFI support (Python, Go, Ruby, etc.) via a shared library.

### Build the shared library

```bash
cargo build --features ffi --release
# Output: target/release/librust_bash.so (Linux), .dylib (macOS), .dll (Windows)
# Header: include/rust_bash.h
```

### Minimal C example

```c
#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/`](examples/ffi/). For the full FFI guide, see the [FFI Usage recipe](docs/recipes/ffi-usage.md).

## Public API (Rust)

| 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 |

## Documentation

- [Guidebook]docs/guidebook/ — architecture, design, and implementation details
- [Recipes]docs/recipes/ — task-oriented guides for common use cases
- [npm package README]packages/core/README.md — TypeScript API reference
- [AI Agent Guide]packages/core/AGENTS.md — quick-start guide for AI agents consuming rust-bash

## Roadmap

The following milestones track the project's progress:

- **Milestone 1–4**: Core interpreter, text processing, execution safety, filesystem backends
-**Milestone 5.1**: Standalone CLI binary — interactive REPL, `-c` commands, script files, stdin piping, `--json` output
-**Milestone 5.2**: C FFI — shared library, generated C header, JSON config, 6 exported functions
-**Milestone 5.3**: WASM target — `wasm32-unknown-unknown`, npm package `rust-bash` with TypeScript types
-**Milestone 5.4**: AI SDK integration — framework-agnostic tool definitions, MCP server, documented adapters
-**Milestone 6**: Shell language completeness — arrays, shopt, process substitution, special variables, advanced redirections, missing builtins, differential testing
-**Milestone 7**: Command coverage & discoverability — 80 commands with `--help`, compression/archiving, search (`rg`), binary inspection, command fidelity infrastructure, AI agent documentation
- Planned: Embedded runtimes — SQLite, yq, Python, JavaScript (M8)
- Planned: Platform features — cancellation, lazy files, AST transforms, fuzz testing (M9)

## License

MIT