skills/bun-runtime-shell/SKILL.md
Use Bun's shell scripting API to run shell commands from JavaScript
npx skillsauth add jarle/bun-skills Bun ShellInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use Bun's shell scripting API to run shell commands from JavaScript
Bun Shell makes shell scripting with JavaScript & TypeScript fun. It's a cross-platform bash-like shell with seamless JavaScript interop.
Quickstart:
import { $ } from "bun";
const response = await fetch("https://example.com");
// Use Response as stdin.
await $`cat < ${response} | wc -c`; // 1256
rimraf or cross-env', you can use Bun Shell without installing extra dependencies. Common shell commands like ls, cd, rm are implemented natively.**, *, {expansion}, and more.Response, ArrayBuffer, Blob, Bun.file(path) and other JavaScript objects as stdin, stdout, and stderr..bun.sh files).The simplest shell command is echo. To run it, use the $ template literal tag:
import { $ } from "bun";
await $`echo "Hello World!"`; // Hello World!
By default, shell commands print to stdout. To quiet the output, call .quiet():
import { $ } from "bun";
await $`echo "Hello World!"`.quiet(); // No output
What if you want to access the output of the command as text? Use .text():
import { $ } from "bun";
// .text() automatically calls .quiet() for you
const welcome = await $`echo "Hello World!"`.text();
console.log(welcome); // Hello World!\n
By default, awaiting will return 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) []
By default, non-zero exit codes will throw an error. This ShellError contains information about the command run.
import { $ } from "bun";
try {
const output = await $`something-that-may-fail`.text();
console.log(output);
} catch (err) {
console.log(`Failed with code ${err.exitCode}`);
console.log(err.stdout.toString());
console.log(err.stderr.toString());
}
Throwing can be disabled with .nothrow(). The result's exitCode will need to be checked manually.
import { $ } from "bun";
const { stdout, stderr, exitCode } = await $`something-that-may-fail`.nothrow().quiet();
if (exitCode !== 0) {
console.log(`Non-zero exit code ${exitCode}`);
}
console.log(stdout);
console.log(stderr);
The default handling of non-zero exit codes can be configured by calling .nothrow() or .throws(boolean) on the $ function itself.
import { $ } from "bun";
// shell promises will not throw, meaning you will have to
// check for `exitCode` manually on every shell command.
$.nothrow(); // equivalent to $.throws(false)
// default behavior, non-zero exit codes will throw an error
$.throws(true);
// alias for $.nothrow()
$.throws(false);
await $`something-that-may-fail`; // No exception thrown
A command's input or output may be redirected using the typical Bash operators:
< redirect stdin> or 1> redirect stdout2> redirect stderr&> redirect both stdout and stderr>> or 1>> 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>&2 redirect stdout to stderr (all writes to stdout will instead be in stderr)2>&1 redirect stderr to stdout (all writes to stderr will instead be in stdout)Bun Shell also supports redirecting from and to JavaScript objects.
>)To redirect stdout to a JavaScript object, use the > operator:
import { $ } from "bun";
const buffer = Buffer.alloc(100);
await $`echo "Hello World!" > ${buffer}`;
console.log(buffer.toString()); // Hello World!\n
The following JavaScript objects are supported for redirection to:
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)<)To redirect the output from JavaScript objects to stdin, use the < operator:
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
The following JavaScript objects are supported for redirection from:
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)import { $ } from "bun";
await $`cat < myfile.txt`;
import { $ } from "bun";
await $`echo bun! > greeting.txt`;
import { $ } from "bun";
await $`bun run index.ts 2> errors.txt`;
import { $ } from "bun";
// redirects stderr to stdout, so all output
// will be available on stdout
await $`bun run ./index.ts 2>&1`;
import { $ } from "bun";
// redirects stdout to stderr, so all output
// will be available on stderr
await $`bun run ./index.ts 1>&2`;
|)Like in bash, you can pipe the output of one command to another:
import { $ } from "bun";
const result = await $`echo "Hello World!" | wc -w`.text();
console.log(result); // 2\n
You can also pipe with 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
$(...))Command substitution allows you to substitute the output of another script into the current script:
import { $ } from "bun";
// Prints out the hash of the current commit
await $`echo Hash of current commit: $(git rev-parse HEAD)`;
This is a textual insertion of the command's output and can be used to, for example, declare a shell variable:
import { $ } from "bun";
await $`
REV=$(git rev-parse HEAD)
docker built -t myapp:$REV
echo Done building docker image "myapp:$REV"
`;
<Note>
Because Bun internally uses the special [`raw`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#raw_strings) property on the input template literal, using the backtick syntax for command substitution won't work:
import { $ } from "bun";
await $`echo \`echo hi\``;
Instead of printing:
hi
The above will print out:
echo hi
We instead recommend sticking to the $(...) syntax.
</Note>
Environment variables can be set like in bash:
import { $ } from "bun";
await $`FOO=foo bun -e 'console.log(process.env.FOO)'`; // foo\n
You can use string interpolation to set environment variables:
import { $ } from "bun";
const foo = "bar123";
await $`FOO=${foo + "456"} bun -e 'console.log(process.env.FOO)'`; // bar123456\n
Input is escaped by default, preventing shell injection attacks:
import { $ } from "bun";
const foo = "bar123; rm -rf /tmp";
await $`FOO=${foo} bun -e 'console.log(process.env.FOO)'`; // bar123; rm -rf /tmp\n
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():
import { $ } from "bun";
await $`echo $FOO`.env({ ...process.env, FOO: "bar" }); // bar
You can change the default environment variables for all commands by calling $.env:
import { $ } from "bun";
$.env({ FOO: "bar" });
// the globally-set $FOO
await $`echo $FOO`; // bar
// the locally-set $FOO
await $`echo $FOO`.env({ FOO: "baz" }); // baz
You can reset the environment variables to the default by calling $.env() with no arguments:
import { $ } from "bun";
$.env({ FOO: "bar" });
// the globally-set $FOO
await $`echo $FOO`; // bar
// the locally-set $FOO
await $`echo $FOO`.env(undefined); // ""
You can change the working directory of a command by passing a string to .cwd():
import { $ } from "bun";
await $`pwd`.cwd("/tmp"); // /tmp
You can change the default working directory for all commands by calling $.cwd:
import { $ } from "bun";
$.cwd("/tmp");
// the globally-set working directory
await $`pwd`; // /tmp
// the locally-set working directory
await $`pwd`.cwd("/"); // /
To read the output of a command as a string, use .text():
import { $ } from "bun";
const result = await $`echo "Hello World!"`.text();
console.log(result); // Hello World!\n
To read the output of a command as JSON, use .json():
import { $ } from "bun";
const result = await $`echo '{"foo": "bar"}'`.json();
console.log(result); // { foo: "bar" }
To read the output of a command line-by-line, use .lines():
import { $ } from "bun";
for await (let line of $`echo "Hello World!"`.lines()) {
console.log(line); // Hello World!
}
You can also use .lines() on a completed command:
import { $ } from "bun";
const search = "bun";
for await (let line of $`cat list.txt | grep ${search}`.lines()) {
console.log(line);
}
To read the output of a command as a Blob, use .blob():
import { $ } from "bun";
const result = await $`echo "Hello World!"`.blob();
console.log(result); // Blob(13) { size: 13, type: "text/plain" }
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 -l for long listing format)rm: remove files and directoriesecho: print textpwd: print the working directorybun: run bun in buncattouchmkdirwhichmvexittruefalseyesseqdirnamebasenamePartially implemented:
mv: move files and directories (missing cross-device support)Not implemented yet, but planned:
Bun Shell also implements a set of utilities for working with shells.
$.braces (brace expansion)This function implements simple brace expansion for shell commands:
import { $ } from "bun";
await $.braces(`echo {1,2,3}`);
// => ["echo 1", "echo 2", "echo 3"]
$.escape (escape strings)Exposes Bun Shell's escaping logic as a function:
import { $ } from "bun";
console.log($.escape('$(foo) `bar` "baz"'));
// => \$(foo) \`bar\` \"baz\"
If you do not want your string to be escaped, wrap it in a { raw: 'str' } object:
import { $ } from "bun";
await $`echo ${{ raw: '$(foo) `bar` "baz"' }}`;
// => bun: command not found: foo
// => bun: command not found: bar
// => baz
.sh file loaderFor simple shell scripts, instead of /bin/sh, you can use Bun Shell to run shell scripts.
To do so, just run the script with bun on a file with the .sh extension.
echo "Hello World! pwd=$(pwd)"
bun ./script.sh
Hello World! pwd=/home/demo
Scripts with Bun Shell are cross platform, which means they work on Windows:
bun .\script.sh
Hello World! pwd=C:\Users\Demo
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.
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:
import { $ } from "bun";
const userInput = "my-file.txt; rm -rf /";
// SAFE: `userInput` is treated as a single quoted string
await $`ls ${userInput}`;
In the above example, 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 /".
While command injection is prevented by default, developers are still responsible for security in certain scenarios.
Similar to the Bun.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.
import { $ } from "bun";
const userInput = "world; touch /tmp/pwned";
// UNSAFE: You have explicitly started a new shell process with `bash -c`.
// This new shell will execute the `touch` command. Any user input
// passed this way must be rigorously sanitized.
await $`bash -c "echo ${userInput}"`;
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.
import { $ } from "bun";
// Malicious input formatted as a Git command-line flag
const branch = "--upload-pack=echo pwned";
// UNSAFE: While Bun safely passes the string as a single argument,
// the `git` program itself sees and acts upon the malicious flag.
await $`git ls-remote origin ${branch}`;
<Note>
**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.
</Note>
Large parts of this API were inspired by zx, dax, and bnx. Thank you to the authors of those projects.
development
Using TypeScript with Bun, including type definitions and compiler options
development
Learn how to write tests using Bun's Jest-compatible API with support for async tests, timeouts, and various test modifiers
testing
Learn how to use snapshot testing in Bun to save and compare output between test runs
testing
Learn about Bun test's runtime integration, environment variables, timeouts, and error handling