Virtual bash interpreter for AI agents. Pure TypeScript, zero runtime dependencies. Runs in any JavaScript runtime - browsers, Node.js, Deno, Bun, and Cloudflare Workers. Ships with 65+ commands, a full jq implementation, and an under 40KB gzipped entry point.
- Pure JS, under 40KB gzipped, zero dependencies, runs anywhere
- 65+ commands including grep, sed, awk, find, xargs, curl, and a full jq implementation
- Pipes, redirections, variables, control flow, functions, arithmetic
- Configurable execution limits, regex guardrails, no eval
- OverlayFs: read-through overlay on real directories with change tracking
npm install @mylocalgpt/shellimport { Shell } from '@mylocalgpt/shell';
const shell = new Shell({
files: { '/data.json': '{"name": "alice"}' },
});
const result = await shell.exec('cat /data.json | jq .name');
console.log(result.stdout); // "alice"\nconst shell = new Shell(options?: ShellOptions);| Option | Type | Description |
|---|---|---|
fs |
FileSystem |
Custom filesystem implementation. When provided, files is ignored. |
files |
Record<string, string | (() => string | Promise<string>)> |
Initial filesystem contents. Values can be strings or lazy-loaded functions. Ignored when fs is provided. |
env |
Record<string, string> |
Environment variables. Merged with defaults (HOME, USER, PATH, SHELL). |
limits |
Partial<ExecutionLimits> |
Execution limits. Merged with safe defaults. |
commands |
Record<string, CommandHandler> |
Custom commands to register. |
onUnknownCommand |
(name, args, ctx) => CommandResult |
Handler for unregistered commands. |
onOutput |
(result) => ExecResult |
Post-processing hook for exec results. |
hostname |
string |
Virtual hostname (used by hostname command). |
username |
string |
Virtual username (used by whoami command). |
enabledCommands |
string[] |
Restrict available commands to this allowlist. |
network |
NetworkConfig |
Network handler for curl. See Network Config. |
onBeforeCommand |
(cmd, args) => boolean | void |
Hook before each command (return false to block). |
onCommandResult |
(cmd, result) => CommandResult |
Hook after each command (can modify result). |
Execute a shell command. Never throws; all errors are returned in the result.
const result = await shell.exec('echo hello', {
env: { EXTRA: 'value' }, // per-call env vars
cwd: '/workspace', // override working directory
stdin: 'input data', // provide stdin
signal: controller.signal, // AbortSignal for cancellation
timeout: 5000, // timeout in milliseconds
});ExecResult:
interface ExecResult {
stdout: string;
stderr: string;
exitCode: number;
}State persistence: environment exports, functions, working directory, and filesystem persist across exec() calls. Shell options (set -e, etc.) reset per call.
Register a custom command that works in pipes and redirections.
shell.defineCommand('fetch-url', async (args, ctx) => {
const response = await fetch(args[0]);
return { stdout: await response.text(), stderr: '', exitCode: 0 };
});
await shell.exec('fetch-url https://api.example.com | jq .data');Clear environment and functions, reset working directory. Filesystem is kept intact.
shell.fs- the virtual FileSystem instanceshell.cwd- current working directoryshell.env- environment variables (Map)
- Pipes and redirections (
|,>,>>,<,2>&1,&>) - Variables and expansion (
$VAR,${VAR:-default},${VAR/pattern/replace}) - Control flow (
if/elif/else/fi,for/while/until,case/esac) - Functions with local variables
- Arrays and arithmetic (
$((expr))) - Command substitution (
$(cmd),`cmd`) - Here documents (
<<EOF) - Glob expansion (
*,?,[...]) - Brace expansion (
{a,b},{1..10}) set -euo pipefail- Subshells (
(cmd)) - Conditional expressions (
[[ ]],[ ])
| Command | Description |
|---|---|
cat |
Concatenate and print files |
cp |
Copy files |
mv |
Move/rename files |
rm |
Remove files |
mkdir |
Create directories |
rmdir |
Remove empty directories |
touch |
Create empty files or update timestamps |
chmod |
Change file permissions |
ln |
Create symbolic links |
stat |
Display file status |
file |
Determine file type |
grep |
Search file contents (-i, -v, -c, -l, -n, -r, -w, -E) |
sed |
Stream editor (s, d, p, i, a, c, y commands; -i, -n, -e) |
awk |
Pattern scanning (print, if/else, variables, field splitting) |
head |
Output first lines (-n, -c) |
tail |
Output last lines (-n, -c, -f) |
sort |
Sort lines (-r, -n, -u, -k, -t) |
uniq |
Filter duplicate lines (-c, -d, -u) |
wc |
Count lines, words, characters (-l, -w, -c, -m) |
cut |
Remove sections from lines (-d, -f, -c) |
tr |
Translate characters (-d, -s) |
rev |
Reverse lines |
tac |
Reverse file line order |
paste |
Merge lines from files (-d) |
fold |
Wrap lines (-w, -s) |
comm |
Compare sorted files |
join |
Join lines on a common field |
nl |
Number lines |
expand |
Convert tabs to spaces |
unexpand |
Convert spaces to tabs |
strings |
Print printable strings |
column |
Format into columns (-t, -s) |
find |
Search for files (-name, -type, -path, -maxdepth) |
xargs |
Build commands from stdin (-I, -n) |
diff |
Compare files |
base64 |
Encode/decode base64 (-d) |
md5sum |
Compute MD5 hash |
sha1sum |
Compute SHA-1 hash |
sha256sum |
Compute SHA-256 hash |
expr |
Evaluate expressions |
od |
Octal dump |
ls |
List directory contents (-l, -a, -R, -1) |
pwd |
Print working directory |
tree |
Display directory tree |
du |
Estimate file space usage |
basename |
Strip directory from path |
dirname |
Strip filename from path |
readlink |
Read symbolic link target |
realpath |
Resolve canonical path |
echo |
Print arguments (-n, -e) |
printf |
Format and print |
env |
Display environment |
printenv |
Print environment variables |
date |
Display date/time |
seq |
Print number sequence |
hostname |
Print hostname |
whoami |
Print current user |
which |
Locate a command |
tee |
Duplicate stdin to file and stdout |
sleep |
Pause execution |
yes |
Repeat a string (output-capped) |
timeout |
Run command with time limit |
xxd |
Hex dump (-l, -s) |
curl |
HTTP requests via network handler |
jq |
JSON processor (full implementation) |
Full jq implementation built from scratch. Supports identity, field access, array/object indexing, pipe, array/object construction, conditionals, try/catch, reduce, foreach, label/break, string interpolation, user-defined functions, and 80+ builtins including map, select, keys, values, has, in, contains, test, match, sub, gsub, split, join, ascii_downcase, ascii_upcase, tostring, tonumber, length, type, empty, error, env, path, getpath, setpath, delpaths, to_entries, from_entries, with_entries, group_by, sort_by, unique_by, min_by, max_by, flatten, range, floor, ceil, round, pow, log, sqrt, nan, infinite, isinfinite, isnan, isnormal, add, any, all, limit, first, last, nth, indices, inside, ltrimstr, rtrimstr, startswith, endswith, ascii, explode, implode, tojson, fromjson, @base64, @base64d, @uri, @csv, @tsv, @html, @json, @text, now, todate, fromdate, strftime, strptime, gmtime, mktime, builtins, debug, input, inputs, $ENV.
Available as a standalone import:
import { jq } from '@mylocalgpt/shell/jq';All limits are configurable via the limits constructor option.
| Limit | Default | Prevents |
|---|---|---|
maxLoopIterations |
10,000 | Infinite loops |
maxCallDepth |
100 | Stack overflow from recursion |
maxCommandCount |
10,000 | Excessive command execution |
maxStringLength |
10,000,000 | Memory exhaustion from string growth |
maxArraySize |
100,000 | Memory exhaustion from array growth |
maxOutputSize |
10,000,000 | Unbounded output accumulation |
maxPipelineDepth |
100 | Deeply nested pipes |
const shell = new Shell({
commands: {
'fetch-data': async (args, ctx) => {
const url = args[0];
const response = await fetch(url);
return { stdout: await response.text(), stderr: '', exitCode: 0 };
},
},
});
// Or after construction:
shell.defineCommand('transform', async (args, ctx) => {
return { stdout: ctx.stdin.toUpperCase(), stderr: '', exitCode: 0 };
});Custom commands receive the full CommandContext and participate in pipes, redirections, and all shell features.
Handle commands not in the registry:
const shell = new Shell({
onUnknownCommand: async (name, args, ctx) => {
if (name === 'python') {
return { stdout: '', stderr: `${name}: use jq for data processing\n`, exitCode: 127 };
}
return { stdout: '', stderr: `${name}: command not found\n`, exitCode: 127 };
},
});Post-process all exec results (truncate, redact, log):
const shell = new Shell({
onOutput: (result) => ({
...result,
stdout: result.stdout.slice(0, 30000), // truncate large output
}),
});Read-through overlay that reads from a real host directory and writes to memory. The host filesystem is never modified.
import { Shell } from '@mylocalgpt/shell';
import { OverlayFs } from '@mylocalgpt/shell/overlay';
const overlay = new OverlayFs('/path/to/project', {
denyPaths: ['*.env', '*.key', 'node_modules/**'],
});
const shell = new Shell({ fs: overlay });
await shell.exec('cat src/index.ts | wc -l');
await shell.exec('echo "new file" > output.txt');
const changes = overlay.getChanges();
// { created: [{ path: '/output.txt', content: 'new file\n' }], modified: [], deleted: [] }Available as a separate entry point at @mylocalgpt/shell/overlay. Requires Node.js (uses node:fs).
curl delegates all HTTP requests to a consumer-provided handler. The shell never makes real network requests.
const shell = new Shell({
network: {
handler: async (url, opts) => {
const res = await fetch(url, { method: opts.method, headers: opts.headers, body: opts.body });
return { status: res.status, body: await res.text(), headers: {} };
},
allowlist: ['api.example.com', '*.internal.corp'],
},
});
await shell.exec('curl -s https://api.example.com/data | jq .results');See THREAT_MODEL.md for the full security model, threat analysis, and explicit non-goals.
What we do:
- All user-provided regex goes through pattern complexity checks and input-length caps
- Execution limits prevent infinite loops, deep recursion, and memory exhaustion
- No
eval()ornew Function()code paths - Path traversal prevention via normalized absolute paths
- Environment variables stored in Map (no prototype pollution)
- No
node:imports; runs in sandboxed environments
What we don't do:
- This is not an OS-level sandbox. The shell executes within your JavaScript runtime's security context.
- Custom commands have full access to the JavaScript environment. Use OS-level isolation for untrusted code.
Apache-2.0