GitHub – vercel-labs/just-bash: Bash for Agents

💥 Discover this insightful post from Hacker News 📖

📂 **Category**:

📌 **What You’ll Learn**:

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 beta software. Use at your own risk and please provide feedback.

  • The shell only has access to the provided file system.
  • Execution is protected against infinite loops or recursion. 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
import 💬 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 execution

Each 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" });

File values can be functions (sync or async). The function is called on first read and the result is cached — if the file is written to before being read, the function is never called:

const env = new Bash({
  files: {
    "/data/config.json": () => JSON.stringify({ key: "value" }),
    "/data/remote.txt": async () => (await fetch("https://example.com")).text(),
    "/data/static.txt": "always loaded",
  },
});

This is useful for large or expensive-to-compute content that may not be needed.

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.

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

OverlayFs – 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 memory

ReadWriteFs – 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 filesystem

MountableFs – Mount multiple filesystems at different paths. Combines read-only and read-write filesystems into a unified namespace:

import { Bash, MountableFs, InMemoryFs } from "just-bash";
import { OverlayFs } from "just-bash/fs/overlay-fs";
import { ReadWriteFs } from "just-bash/fs/read-write-fs";

const fs = new MountableFs({ base: new InMemoryFs() });

// Mount read-only knowledge base
fs.mount("/mnt/knowledge", new OverlayFs({ root: "/path/to/knowledge", readOnly: true }));

// Mount read-write workspace
fs.mount("/home/agent", new ReadWriteFs({ root: "/path/to/workspace" }));

const bash = new Bash({ fs, cwd: "/home/agent" });

await bash.exec("ls /mnt/knowledge"); // reads from knowledge base
await bash.exec("cp /mnt/knowledge/doc.txt ./"); // cross-mount copy
await bash.exec('echo "notes" > notes.txt'); // writes to workspace

You can also configure mounts in the constructor:

import { MountableFs, InMemoryFs } from "just-bash";
import { OverlayFs } from "just-bash/fs/overlay-fs";
import { ReadWriteFs } from "just-bash/fs/read-write-fs";

const fs = new MountableFs({
  base: new InMemoryFs(),
  mounts: [
    { mountPoint: "/data", filesystem: new OverlayFs({ root: "/shared/data" }) },
    { mountPoint: "/workspace", filesystem: new ReadWriteFs({ root: "/tmp/work" }) },
  ],
});

For AI agents, use bash-tool which is optimized for just-bash and provides a ready-to-use AI SDK tool:

import { createBashTool } from "bash-tool";
import { generateText } from "ai";

const bashTool = createBashTool({
  files: { "/data/users.json": '[{"name": "Alice"}, {"name": "Bob"}]' },
});

const result = await generateText({
  model: "anthropic/claude-sonnet-4",
  tools: { bash: bashTool },
  prompt: "Count the users in /data/users.json",
});

See the bash-tool documentation for more details and examples.

Vercel Sandbox Compatible API

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