Features
- Cross-platform: works on Windows, Linux & macOS. Instead of
rimraforcross-env’, you can use Bun Shell without installing extra dependencies. Common shell commands likels,cd,rmare implemented natively. - Familiar: Bun Shell is a bash-like shell, supporting redirection, pipes, environment variables and more.
- Globs: Glob patterns are supported natively, including
**,*,{expansion}, and more. - Template literals: Template literals execute shell commands, allowing interpolation of variables and expressions.
- Safety: Bun Shell escapes all strings by default, preventing shell injection attacks.
- JavaScript interop: Use
Response,ArrayBuffer,Blob,Bun.file(path)and other JavaScript objects as stdin, stdout, and stderr. - Shell scripting: Bun Shell can be used to run shell scripts (
.bun.shfiles). - Custom interpreter: Bun Shell is written in Zig, along with its lexer, parser, and interpreter. Bun Shell is a small programming language.
Getting started
The simplest shell command isecho. To run it, use the $ template literal tag:
.quiet():
.text():
awaiting will return stdout and stderr as Buffers.
Error handling
By default, non-zero exit codes will throw an error. ThisShellError contains information about the command run.
.nothrow(). The result’s exitCode will need to be checked manually.
.nothrow() or .throws(boolean) on the $ function itself.
Redirection
A command’s input or output may be redirected using the typical Bash operators:<redirect stdin>or1>redirect stdout2>redirect stderr&>redirect both stdout and stderr>>or1>>redirect stdout, appending to the destination, instead of overwriting2>>redirect stderr, appending to the destination, instead of overwriting&>>redirect both stdout and stderr, appending to the destination, instead of overwriting1>&2redirect stdout to stderr (all writes to stdout will instead be in stderr)2>&1redirect stderr to stdout (all writes to stderr will instead be in stdout)
Example: Redirect output to JavaScript objects (>)
To redirect stdout to a JavaScript object, use the > operator:
Buffer,Uint8Array,Uint16Array,Uint32Array,Int8Array,Int16Array,Int32Array,Float32Array,Float64Array,ArrayBuffer,SharedArrayBuffer(writes to the underlying buffer)Bun.file(path),Bun.file(fd)(writes to the file)
Example: Redirect input from JavaScript objects (<)
To redirect the output from JavaScript objects to stdin, use the < operator:
Buffer,Uint8Array,Uint16Array,Uint32Array,Int8Array,Int16Array,Int32Array,Float32Array,Float64Array,ArrayBuffer,SharedArrayBuffer(reads from the underlying buffer)Bun.file(path),Bun.file(fd)(reads from the file)Response(reads from the body)
Example: Redirect stdin -> file
Example: Redirect stdout -> file
Example: Redirect stderr -> file
Example: Redirect stderr -> stdout
Example: Redirect stdout -> stderr
Piping (|)
Like in bash, you can pipe the output of one command to another:
Command substitution ($(...))
Command substitution allows you to substitute the output of another script into the current script:
Because Bun internally uses the special Instead of printing:The above will print out:We instead recommend sticking to the
raw property on the input template literal, using the backtick syntax for command substitution won’t work:$(...) syntax.Environment variables
Environment variables can be set like in bash:Changing the environment variables
By default,process.env is used as the environment variables for all commands.
You can change the environment variables for a single command by calling .env():
$.env:
$.env() with no arguments:
Changing the working directory
You can change the working directory of a command by passing a string to.cwd():
$.cwd:
Reading output
To read the output of a command as a string, use.text():
Reading output as JSON
To read the output of a command as JSON, use.json():
Reading output line-by-line
To read the output of a command line-by-line, use.lines():
.lines() on a completed command:
Reading output as a Blob
To read the output of a command as a Blob, use.blob():
Builtin Commands
For cross-platform compatibility, Bun Shell implements a set of builtin commands, in addition to reading commands from the PATH environment variable.cd: change the working directoryls: list files in a directory (supports-lfor long listing format)rm: remove files and directoriesecho: print textpwd: print the working directorybun: run bun in buncattouchmkdirwhichmvexittruefalseyesseqdirnamebasename
mv: move files and directories (missing cross-device support)
- See Issue #9716 for the full list.
Utilities
Bun Shell also implements a set of utilities for working with shells.$.braces (brace expansion)
This function implements simple brace expansion for shell commands:
$.escape (escape strings)
Exposes Bun Shell’s escaping logic as a function:
{ raw: 'str' } object:
.sh file loader
For simple shell scripts, instead of /bin/sh, you can use Bun Shell to run shell scripts.
To do so, run the script with bun on a file with the .sh extension.
script.sh
terminal
powershell
Implementation notes
Bun Shell is a small programming language in Bun that is implemented in Zig. It includes a handwritten lexer, parser, and interpreter. Unlike bash, zsh, and other shells, Bun Shell runs operations concurrently.Security in the Bun shell
By design, the Bun shell does not invoke a system shell (like/bin/sh) and
is instead a re-implementation of bash that runs in the same Bun process,
designed with security in mind.
When parsing command arguments, it treats all interpolated variables as single, literal strings.
This protects the Bun shell against command injection:
userInput is treated as a single string. This causes
the ls command to try to read the contents of a single directory named
“my-file; rm -rf /”.
Security considerations
While command injection is prevented by default, developers are still responsible for security in certain scenarios. Similar to theBun.spawn or node:child_process.exec() APIs, you can intentionally
execute a command which spawns a new shell (e.g. bash -c) with arguments.
When you do this, you hand off control, and Bun’s built-in protections no
longer apply to the string interpreted by that new shell.
Argument injection
The Bun shell cannot know how an external command interprets its own command-line arguments. An attacker can supply input that the target program recognizes as one of its own options or flags, leading to unintended behavior.Recommendation — As is best practice in every language, always sanitize user-provided input before passing it as
an argument to an external command. The responsibility for validating arguments rests with your application code.
Sandboxed shells (experimental)
$.sandbox(options) creates a restricted shell for running untrusted shell commands. Because Bun Shell implements its commands natively, the policy is enforced inside the interpreter, before any command or filesystem operation executes.
$-compatible: it supports interpolation, .cwd(), .env(), .quiet(), .nothrow(), .text(), and friends, and inherits the cwd/env/throws configuration of the shell it derives from (Bun.$ or a new $.Shell() instance). A sandboxed shell cannot be re-sandboxed; derive a new sandbox from an unsandboxed shell instead.
Blocked commands and file operations fail with exit code 1 and a ... not permitted in sandbox message on stderr, so they compose with &&, ||, and .nothrow() like any other failing command. Exceeded limits reject the promise with a descriptive error.
Command policy
Only Bun Shell builtins can run inside a sandbox — external binaries are always blocked, before anyPATH lookup happens. commands.allow and commands.deny restrict the builtin set further:
allow/deny throw, listing the valid builtin names. The available builtin set matches the regular shell’s: on POSIX platforms cat and cp currently delegate to system binaries, so inside a sandbox they are blocked like any external command.
Because external binaries cannot run, environment tricks like PATH=/evil cmd cannot smuggle a command in, and which never probes the host filesystem — it only reports policy-permitted builtins.
Filesystem policy
By default a sandboxed shell has no filesystem access.fs.read and fs.write grant access under absolute path prefixes; a write prefix also grants read. Omit write for a read-only sandbox.
Every path the script touches — command operands, redirect targets, glob walks, cd, and [[ -f ... ]] tests — is resolved against the shell’s working directory and through symlinks before it is compared against the prefixes. This is what stops the classic escapes:
[[ -f /etc/passwd ]] answer as if the file does not exist, so scripts cannot probe the host tree. Recursive operations (ls -R, rm -r) stay under their checked roots because traversal never follows symlinks.
Paths granted to the policy and paths checked at runtime are both canonicalized the same way (symlinks resolved via the deepest existing ancestor), so grants work even when the directory itself is reached through a symlink, such as /tmp on macOS.
Network policy
A sandboxed script cannot reach the network: external binaries are blocked, and no shell builtin performs network I/O. Thenetwork option is reserved for future allowlists and currently only accepts false (the default); passing true throws.
Limits
limits.timeout— wall-clock limit in milliseconds for the whole script. When it expires, the promise rejects withShell command timed out after <n>ms (sandbox limits.timeout)and the interpreter stops scheduling work.limits.maxOutputBytes— caps the total bytes the script may write to stdout, stderr, and file redirects combined. When exceeded, the promise rejects withShell command output exceeded <n> bytes (sandbox limits.maxOutputBytes).
error.stdout / error.stderr.
Current limitations
This API is experimental, and the policy surface is designed to grow (finer-grained network rules and an overlay filesystem where writes land in a scratch layer are planned). Current limitations to be aware of:- JavaScript objects interpolated into the command (
Bun.file(),Response, buffers) are provided by the calling code, not the untrusted script, so they are intentionally not subject to the filesystem policy. - A builtin blocked forever on a pipe that never receives data keeps its read poll active after a timeout; the promise still rejects and the caller regains control.
- Path checks happen when an operation starts. With external binaries disabled, scripts cannot race the interpreter from another thread, but processes outside the sandbox mutating the same tree concurrently can still change what a checked path points at.