Check it out here: https://www.npmjs.com/package/@archildata/just-bash
I wonder the reason.
Maybe 'do one thing well'? The piping? The fact that the tools have been around so long so there are so many examples in the training data? Simplicity? All of it?
The success of this project depends on the answer.
Even so, I suspect that something like this will be a far too leaky abstraction.
But Vercel must try because they see the writing on the wall.
No one needs expensive cloud platforms anymore.
https://github.com/jeffchuber/just-bash-openfs
it puts a bash interface in front of s3, filesystem (real and in-memory), postgres, and chroma.
still very much alpha - but curious what people think about it.
see an example app here: https://github.com/jeffchuber/openfs-incident-app
Bash has been unchanged for decades but its not a very nice language.
I know pydantic has been experimenting with https://github.com/pydantic/monty (restricted python) and I think Cloudflare and co were experimenting with giving typescript to agents.
That's a lot of incompatibilities.
LLMs like to use the shell because it's stable and virtually unchanged for decades.
It doesn't need to worry much about versions or whether something is supported or not, it can just assume it is.
Re-implementing bash is a herculean effort. I wish good luck.
I'm not going for compatibility, but something that is a bit hackable. Deliberately not having /lib /share and /etc to avoid confusion that it might be posix
On neocoties for proof of static hosting
[Disclaimer: I made the thing]
I can trivially combine a tool written in rust with one written in js/java/C/whatever without writing bindings
So, mostly re-enforcement along multiple vectors.
There's also the maintenance of the server to be considered. Vercel or other PaaS/Lambda/GCP functions/etc serverless means there's just less crap for me to manage, because they're dealing with it, and yeah, they charge money for that service. Being able to tell Claude code, I setup ssh keys and sudo no password for you, go fix my shit; like, that works, but then the hard drive is full so I have to up size the VPS, and if you're stupid/brave, you can give Claude Code MCP access to Chrome so it can click the buttons in Hetzner to upsize for you, but that's time and tokens spent not working on the product so at the end of the day I think Vercel is gonna be fine. AI generating code means there are many many more people trying out making some sort of Internet company, but they'll only discover cheaper options only after paying for Vercel becomes painful.
> std::slop is a persistent, SQLite-driven C++ CLI agent. It remembers your work through per-session ledgers, providing long-term recall, structured state management. std::slop features built-in Git integration. It's goal is to be an agent for which the context and its use fully transparent and configurable.
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.
npm install just-bash
import { 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 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:
npm install bash-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.
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 JSONpnpm shell
The 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-network
cat, cp, file, ln, ls, mkdir, mv, readlink, rm, rmdir, split, stat, touch, tree
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
jq (JSON), python3/python (Python via Pyodide; required opt-in), sqlite3 (SQLite), xan (CSV), yq (YAML/XML/TOML/CSV)
gzip (+ gunzip, zcat), tar
basename, cd, dirname, du, echo, env, export, find, hostname, printenv, pwd, tee
alias, bash, chmod, clear, date, expr, false, help, history, seq, sh, sleep, time, timeout, true, unalias, which, whoami
curl, html-to-markdown
All commands support --help for usage information.
cmd1 | cmd2>, >>, 2>, 2>&1, <&&, ||, ;$VAR, ${VAR}, ${VAR:-default}$1, $2, $@, $#*, ?, [...]if COND; then CMD; elif COND; then CMD; else CMD; fifunction name { ... } or name() { ... }local VAR=valuefor, while, untilln -s target linkln target linkWhen 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 directoryCommands 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".
Python support via Pyodide is opt-in due to additional security surface. Enable it explicitly, but be aware of the risk:
const env = new Bash({
python: true,
});
// Execute Python code
await env.exec('python3 -c "print(1 + 2)"');
// Run Python scripts
await env.exec('python3 script.py');
Note: The python3 and python commands only exist when python: true is configured. Python is not available in browser environments.
The sqlite3 command uses sql.js (WASM-based SQLite) which is fully sandboxed and cannot access the real filesystem:
const env = new Bash();
// Query in-memory database
await env.exec('sqlite3 :memory: "SELECT 1 + 1"');
// Query file-based database
await env.exec('sqlite3 data.db "SELECT * FROM users"');
Note: SQLite is not available in browser environments. Queries run in a worker thread with a configurable timeout (default: 5 seconds) to prevent runaway queries from blocking execution.
The allow-list enforces:
allowedMethods for more)# 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/endpoint
Bash 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.
Parse bash scripts into an AST, run transform plugins, and serialize back to executable bash. Useful for instrumenting scripts (e.g., capturing per-command stdout/stderr) or analyzing them (e.g., extracting command names) before execution.
import { Bash, BashTransformPipeline, TeePlugin, CommandCollectorPlugin } from "just-bash";
// Standalone pipeline — output can be run by any shell
const pipeline = new BashTransformPipeline()
.use(new TeePlugin({ outputDir: "/tmp/logs" }))
.use(new CommandCollectorPlugin());
const result = pipeline.transform("echo hello | grep hello");
result.script; // transformed bash string
result.metadata.commands; // ["echo", "grep", "tee"]
// Integrated API — exec() auto-applies transforms and returns metadata
const bash = new Bash();
bash.registerTransformPlugin(new CommandCollectorPlugin());
const execResult = await bash.exec("echo hello | grep hello");
execResult.metadata?.commands; // ["echo", "grep"]
See src/transform/README.md for the full API, built-in plugins, and how to write custom plugins.
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 shell
For AI agents, we recommend using bash-tool which is optimized for just-bash and provides additional guidance in its AGENTS.md:
cat node_modules/bash-tool/dist/AGENTS.md
Apache-2.0
but i think its still useful if we are bound to js/ts ecosystem sandboxed enviroment like in vercel.
In practice it works great. I haven't seen a failed command in a while
[Disclaimer: I made the thing]
Ideally something like nushell but they don't know that well
pro-tip: vercel's https://agent-browser.dev/ is a great CLI for agent-based browser automation.
People do care.
> You only need to be picky with language if a human is going to be working with the code.
Sooner or later humans will have to work with the code - if only for their own self-preservation.
> I get the impression that is not the use case here though
If that's not the use case, there's no legitimate use case at all.
Also, huge waste of tokens. And the waste is not even worth it, the sandbox seems insufficient.
Again, good luck to the developers. I just don't think it's ready.
TypeScript is just a language anyway. It's the runtime that needs to be contained. In that sense it's no different from any other interpreter or runtime, whether it be Go, Python, Java, or any shell.
In my view this really is best managed by the OS kernel, as the ultimate responsibility for process isolation belongs to it. Relying on userspace solutions to enforce restrictions only gets you so far.
Gotta work with what's in the training data I suppose.
I agree on all counts and that this project is silly on the face of it.
My comment was more that there is a massive cohort of devs who have never done sysadmin and know nothing of prior art in the space. Typescript "feels" safe and familiar and the right way to accomplish their goals, regardless of if it actually is.
https://github.com/alganet/coral
busybox, bash, zsh, dash, you name it. If smells bourne, it runs. Here's the list: https://github.com/alganet/coral/blob/main/test/matrix#L50 (more than 20 years of compatibility, runs even on bash 3)
It's a great litmus test, that many have passed. Let me know when just-bash is able to run it.
Anyway, it was rethorical. I was making a point about portability. Scripts we write today run even on ancient versions, and it has been an effort kept by lots of different interpreters (not only bash).
I'm trying to give sane advice here. Re-implementing bash is a herculean task, and some "small incompatibilities" sometimes reveal themselves as deep architectural dead-ends.
> they use it because there's a lot of training material.
Now, you say:
> they are not trying to re-use existing bash code.
Can't you see how this is a contradiction?
---
I'm sorry, I can't continue like this. I want to have meaningful conversations.