Skip to main content
Bun Shell makes shell scripting with JavaScript and TypeScript ergonomic and safe. It is a cross-platform bash-like shell with seamless JavaScript interop — no extra dependencies required.
import { $ } from "bun";

const response = await fetch("https://example.com");

// Use a Response object as stdin
await $`cat < ${response} | wc -c`; // 1256

Features

Cross-platform

Works on Windows, Linux, and macOS. Common commands like ls, cd, rm are implemented natively — no cross-env or rimraf needed.

Safe by default

All interpolated values are treated as literal strings, preventing shell injection attacks.

JavaScript interop

Use Response, Blob, ArrayBuffer, Bun.file(), and other JS objects directly as stdin/stdout.

Familiar syntax

Supports redirection, pipes, globs, environment variables, and command substitution just like bash.

Basic usage

Run commands using the $ tagged template literal. By default, output goes to stdout:
import { $ } from "bun";

await $`echo "Hello World!"`; // Hello World!

Capturing output

Use .text() to capture stdout as a string:
import { $ } from "bun";

const output = await $`echo "Hello World!"`.text();
console.log(output); // Hello World!\n
Use .quiet() to suppress output without capturing it:
import { $ } from "bun";

await $`echo "Hello World!"`.quiet(); // No output printed

Capturing stdout and stderr as buffers

Awaiting a shell command directly returns an object with stdout and stderr as Buffers:
import { $ } from "bun";

const { stdout, stderr } = await $`echo "Hello!"`.quiet();

console.log(stdout); // Buffer(7) [ 72, 101, 108, 108, 111, 33, 10 ]
console.log(stderr); // Buffer(0) []

Other output formats

import { $ } from "bun";

const result = await $`echo '{"foo": "bar"}'`.json();
console.log(result); // { foo: "bar" }

Error handling

By default, commands that exit with a non-zero code throw a ShellError:
import { $ } from "bun";

try {
  await $`some-command-that-fails`.text();
} catch (err) {
  console.log(`Failed with code ${err.exitCode}`);
  console.log(err.stdout.toString());
  console.log(err.stderr.toString());
}

Disabling throws

Use .nothrow() on a single command to check exitCode manually instead of catching:
import { $ } from "bun";

const { stdout, stderr, exitCode } = await $`some-command`.nothrow().quiet();

if (exitCode !== 0) {
  console.log(`Non-zero exit code: ${exitCode}`);
}
Use $.nothrow() or $.throws(false) to change the default behavior for all commands:
import { $ } from "bun";

// All commands will not throw on non-zero exit
$.nothrow(); // equivalent to $.throws(false)

await $`something-that-may-fail`; // No exception thrown

// Restore default behavior
$.throws(true);

Piping

Pipe the output of one command to another using |, just like in bash:
import { $ } from "bun";

const result = await $`echo "Hello World!" | wc -w`.text();
console.log(result); // 2\n
You can also pipe from JavaScript objects:
import { $ } from "bun";

const response = new Response("hello i am a response body");
const result = await $`cat < ${response} | wc -w`.text();
console.log(result); // 6\n

Redirection

Bun Shell supports all standard bash redirection operators, as well as redirecting to and from JavaScript objects.
OperatorDescription
<Redirect stdin
> or 1>Redirect stdout (overwrite)
2>Redirect stderr (overwrite)
&>Redirect stdout and stderr
>>Redirect stdout (append)
2>>Redirect stderr (append)
2>&1Redirect stderr to stdout
1>&2Redirect stdout to stderr

Redirect to a JavaScript object

import { $ } from "bun";

const buffer = Buffer.alloc(100);
await $`echo "Hello World!" > ${buffer}`;
console.log(buffer.toString()); // Hello World!\n
Supported targets: Buffer, typed arrays, ArrayBuffer, SharedArrayBuffer, Bun.file(path), Bun.file(fd).

Redirect from a JavaScript object

import { $ } from "bun";

const response = new Response("hello i am a response body");
const result = await $`cat < ${response}`.text();
console.log(result); // hello i am a response body
Supported sources: Buffer, typed arrays, ArrayBuffer, SharedArrayBuffer, Bun.file(), Response.

File redirection examples

import { $ } from "bun";

await $`echo bun! > greeting.txt`;

Environment variables

Inline env vars

Set environment variables inline just like in bash:
import { $ } from "bun";

await $`FOO=bar bun -e 'console.log(process.env.FOO)'`; // bar
Interpolated values are escaped automatically, preventing injection:
import { $ } from "bun";

const userInput = "bar; rm -rf /tmp";
// SAFE: userInput is treated as a single literal string
await $`FOO=${userInput} bun -e 'console.log(process.env.FOO)'`;
// => bar; rm -rf /tmp

Per-command env with .env()

Override environment variables for a single command:
import { $ } from "bun";

await $`echo $FOO`.env({ ...process.env, FOO: "bar" }); // bar

Global env with $.env()

Set default environment variables for all commands:
import { $ } from "bun";

$.env({ FOO: "bar" });

await $`echo $FOO`;                   // bar
await $`echo $FOO`.env({ FOO: "baz" }); // baz (local override)
await $`echo $FOO`.env(undefined);    // "" (reset to empty)

Working directory

Per-command .cwd()

import { $ } from "bun";

await $`pwd`.cwd("/tmp"); // /tmp

Global $.cwd()

import { $ } from "bun";

$.cwd("/tmp");

await $`pwd`;          // /tmp
await $`pwd`.cwd("/"); // / (local override)

Command substitution

Use $(...) to insert the output of one command into another:
import { $ } from "bun";

await $`echo Hash of current commit: $(git rev-parse HEAD)`;
import { $ } from "bun";

await $`
  REV=$(git rev-parse HEAD)
  docker build -t myapp:$REV .
  echo Done building docker image "myapp:$REV"
`;
Use the $(...) syntax for command substitution. Due to how Bun uses the raw property on template literals, the backtick syntax (`...`) does not work for nested command substitution.

Builtin commands

Bun Shell ships native implementations of common commands for cross-platform compatibility: cd, ls, rm, echo, pwd, bun, cat, touch, mkdir, which, mv, exit, true, false, yes, seq, dirname, basename Any command not in this list is looked up in the system PATH.

Running .sh scripts

Use Bun to run .sh files directly. Bun Shell interprets them cross-platform:
script.sh
echo "Hello World! pwd=$(pwd)"
bun ./script.sh
# Hello World! pwd=/home/demo
This also works on Windows:
bun .\script.sh
# Hello World! pwd=C:\Users\Demo

Utilities

$.braces — brace expansion

import { $ } from "bun";

await $.braces(`echo {1,2,3}`);
// => ["echo 1", "echo 2", "echo 3"]

$.escape — escape strings

import { $ } from "bun";

console.log($.escape('$(foo) `bar` "baz"'));
// => \$(foo) \`bar\` \"baz\"
To pass a raw (unescaped) string, wrap it in { raw: '...' }:
import { $ } from "bun";

await $`echo ${{ raw: '$(foo) `bar` "baz"' }}`;

Security

Bun Shell does not invoke a system shell like /bin/sh. It is a re-implementation that treats all interpolated variables as single, literal strings, preventing command injection:
import { $ } from "bun";

const userInput = "my-file.txt; rm -rf /";

// SAFE: userInput is treated as a single quoted string
await $`ls ${userInput}`;
Spawning a new shell process bypasses Bun’s protections. If you use bash -c with interpolated user input, you are responsible for sanitizing that input:
// UNSAFE: the new bash process interprets the string as shell syntax
const userInput = "world; touch /tmp/pwned";
await $`bash -c "echo ${userInput}"`;
Argument injection is not prevented. Bun passes strings as single arguments, but the target program may interpret them as its own flags:
const branch = "--upload-pack=echo pwned";
// Bun safely passes the string, but git acts on the malicious flag
await $`git ls-remote origin ${branch}`;
Always validate and sanitize user-provided input before passing it to external commands.

Build docs developers (and LLMs) love