BashBox is a bash interpreter written in pure PHP by Pushpak Chhajed, who also built the Grant roles and permissions package. It runs scripts that AI agents generate without calling proc_open, exec, or shell_exec. Each command is a PHP class that works against a virtual filesystem, and every execution runs under limits you can configure.
Running Scripts
Create a Bash instance and pass a script to exec(). The result holds the script's output and exit code:
use BashBox\Bash; $bash = new Bash; $result = $bash->exec(' NAME="BashBox" echo "Hello, $NAME" | tr a-z A-Z'); $result->stdout; // "HELLO, BASHBOX\n"$result->exitCode; // 0
The interpreter supports pipes, fd redirections such as 2>&1, here-docs, process substitution, arrays, functions, control flow, parameter expansion, and set/shopt options. It ships 58 commands, including grep, sed, sort, find, xargs, and sha256sum, plus builtins like cd, export, source, read, and trap. There is no sh or bash binary to call. A script that fails to parse returns exit code 2 with bash's syntax error on stderr instead of throwing an exception.
You can seed the sandbox with files and environment variables when you create it:
use BashBox\Bash;use BashBox\BashOptions; $bash = new Bash(new BashOptions( initialFiles: [ '/home/user/app.log' => "INFO boot\nERROR db timeout\nINFO ready\n", ], env: ['APP_ENV' => 'production'],)); $bash->exec('grep ERROR app.log')->stdout; // "ERROR db timeout\n"
For a single call, ExecOptions passes stdin, extra environment variables, or different limits to exec().
Filesystem Backends
The default InMemoryFs never touches disk. Three other backends give a script access to real files:
OverlayFsreads from a real directory and keeps writes in an in-memory copy-on-write layerReadWriteFsreads and writes to disk, confined to a root directoryMountableFsmounts different backends at different paths
use BashBox\Filesystem\InMemoryFs;use BashBox\Filesystem\MountableFs;use BashBox\Filesystem\OverlayFs;use BashBox\Filesystem\ReadWriteFs; // Read a real project, keep all writes in memory$bash = new Bash(new BashOptions( fs: new OverlayFs('/path/to/project'),)); // Keep everything in memory except a writable /data mount$mount = new MountableFs(new InMemoryFs);$mount->mount('/data', new ReadWriteFs('/real/data'));$bash = new Bash(new BashOptions(fs: $mount));
Each backend has a disk quota of 64 MB and 10,000 entries by default. A write past the quota fails with No space left on device, as it would on a full disk. BashBox also blocks path traversal and null-byte injection, and OverlayFs denies symlinks by default when it reads from a real directory.
Execution Limits
The Limits class has 18 settings, from command count and loop iterations to here-doc size and parse tree depth. Going over one throws BashBox\Exceptions\ExecutionLimitException:
use BashBox\Limits; $bash = new Bash(new BashOptions( limits: new Limits( maxCommandCount: 100, maxLoopIterations: 500, maxOutputSize: 1024 * 1024, // 1MB maxCallDepth: 10, ),));
The defaults allow 10,000 commands per script, 10,000 iterations per loop, 10 MB of output, a function call depth of 100, and 1 MB of script text. ExecOptions can override the limits for one exec() call, except the filesystem quota, which is set when you create the Bash instance.
Network Access
Network access is off by default, and the curl command does not exist until you pass a NetworkConfig:
use BashBox\Network\NetworkConfig; $bash = new Bash(new BashOptions( network: new NetworkConfig( allowedUrlPrefixes: ['https://api.example.com/'], allowedMethods: ['GET', 'POST'], denyPrivateRanges: true, maxResponseSize: 5 * 1024 * 1024, // 5MB timeout: 10, ),));
Scripts can then call curl on any URL under an allowed prefix. The denyPrivateRanges option is on by default. It refuses loopback, private, link-local, and other non-public IPv4 and IPv6 addresses, and checks both the URL's host and the address curl connects to. Like real curl, BashBox's curl follows redirects only with -L. It checks each redirect target against the allow-list and drops Authorization and Cookie headers on cross-origin redirects. An empty allowedUrlPrefixes list denies every URL. To allow any URL, set dangerouslyAllowFullInternetAccess: true.
Custom Commands
To add a command BashBox doesn't ship, extend AbstractCommand and register it on the Bash instance:
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); $bash->exec('mycommand')->stdout; // "Hello from my command!"
Installation
BashBox requires PHP 8.4 or later and has no other Composer dependencies. You can call it from a tool in the Laravel AI SDK or any other agent library and return the output to the model. With the optional ffi extension, BashBox uses the RE2 regex engine. Without it, BashBox uses PCRE with strict limits.
composer require shipfastlabs/bashbox
BashBox is MIT licensed, and the latest release is v0.0.3. You can find the source and full documentation in the BashBox repository on GitHub.