- name
- bun
- description
- Write and review Bun: the Bun-native API surface against its `node:` equivalents, the HTTP and WebSocket server, file and process I/O, the shell, the data clients, `bun:test`, the bundler and compiled binaries, and the package manager.
- when_to_use
- Invoke whenever Bun code or a Bun project is touched at all — writing, reviewing, refactoring, debugging, or configuring one, or deciding whether a `Bun.*` API replaces a dependency. Also invoke on the symptoms: a request dies after ten seconds with no error, a stack trace or an error page reaches the browser, `postinstall` stops running for a package that used to build, a mocked module still ran its side effects, `bun install` lays out `node_modules` differently than the last machine did, a compiled binary reads a config file it should not, or a `node:` API behaves differently under Bun than under Node. Covers the Bun runtime and its toolchain, `bun:test` included; JavaScript semantics belong to the javascript skill, types to typescript, Node's own APIs to nodejs, and Vitest to vitest.
- compatibility
- Uses Claude Code frontmatter beyond the Agent Skills spec (when_to_use)
Bun's value is that the runtime, the toolchain, and the batteries are one binary. Two biases decide most calls:
- **Reach for what already ships.** A dependency earns its place by doing what `Bun.*`, `bun:*`, and the bundled
toolchain do not. Adding one that duplicates a built-in is a defect.
- **Write `node:` only where Bun has no native equivalent, or where the code must also run under Node.** Portability is
a requirement to state, not a habit to keep.
## Release Version
Bun ships one stable line and no long-lived support branches, so `bun upgrade` moves a machine to whatever release is
newest. Pin the version in CI (`oven-sh/setup-bun`, `bun-version`) so a developer machine and a runner agree, and state
the release a feature needs in the sentence that uses it. Never write "the latest version supports X".
The rules below assume 1.3.0 or later unless a version anchor says otherwise. Floors for the features these rules
reference:
- **1.3.0** — `Bun.sql` for MySQL, MariaDB, and SQLite; `Bun.redis`; `Bun.secrets`; `Bun.YAML`; catalogs;
`test.concurrent`, `test.serial`, and `--concurrent`; `--randomize`; `expectTypeOf`
- **1.3.2** — the isolated linker as the default for new monorepos; `nativeDependencies`; `ignoreScripts`
- **1.3.3** — `test(..., { retry })`
- **1.3.4** — `jest.useFakeTimers()`; `URLPattern`; `--compile` stops auto-loading `tsconfig.json` and `package.json`
- **1.3.5** — `Bun.Terminal`; `Bun.stringWidth`; `bun:bundle` feature flags; `trustedDependencies` limited to npm
sources
- **1.3.6** — `Bun.JSONC`; `Bun.Archive`; `Bun.build({ files })`; `metafile: true`
- **1.3.7** — `Bun.JSON5`; `Bun.JSONL`; `Bun.wrapAnsi`
- **1.3.8** — `Bun.markdown`; `--metafile-md`
- **1.3.9** — `bun run --parallel`; bytecode for ES modules
- **1.3.10** — `optimizeImports`; standard TC39 decorators; `--compile --target=browser` for a single HTML file
- **1.3.11** — `Bun.cron()`; `Bun.sliceAnsi`
- **1.3.12** — `Bun.WebView`
- **1.3.13** — `bun test --parallel`, `--isolate`, `--shard`, `--changed`; `Range` and conditional requests in
`Bun.serve`
- **1.3.14** — `Bun.Image`; the global virtual store; HTTP/3 in `Bun.serve`; HTTP/2 and HTTP/3 in `fetch()`
- **1.4.0** — Node 26 compatibility; `Bun.XML`; `Bun.isStandaloneExecutable`; `Bun.spawn({ cgroup })`; directory routes;
`bun audit fix`, `bun dedupe`, `bun prune`, `bun pm diff`, `bun pm licenses`; `bun test --timings`; `--asset`;
`lockfileVersion: 2`
## Prefer the Built-in
Reach for the built-in, and drop the package it replaces from `package.json`.
- **HTTP server** — `Bun.serve()` over `http.createServer()`, Express, or Fastify
- **Files** — `Bun.file()` and `Bun.write()` over `node:fs` read and write helpers
- **Directories** — `node:fs`. There is no `Bun.*` equivalent for `mkdir` or `readdir`
- **Globbing** — `Bun.Glob` over `glob` and `fast-glob`
- **Shelling out** — `Bun.$` over `child_process.exec`, `zx`, `execa`, `cross-env`, and `rimraf`
- **Processes** — `Bun.spawn` and `Bun.spawnSync` over `child_process.spawn`
- **Pseudo-terminals** — `Bun.Terminal` over `node-pty` (1.3.5)
- **SQLite** — `bun:sqlite` over `better-sqlite3`
- **Postgres, MySQL, MariaDB** — `Bun.sql` over `pg` and `mysql2` (1.3.0)
- **Redis and Valkey** — `Bun.redis` over `ioredis` (1.3.0)
- **S3-compatible storage** — `Bun.S3Client` over `@aws-sdk/client-s3`
- **Password hashing** — `Bun.password` over `bcrypt` and `argon2`
- **Testing** — `bun:test` over Jest
- **Bundling** — `Bun.build()` and `bun build` over webpack, Rollup, and esbuild
- **Images** — `Bun.Image` over `sharp` (1.3.14)
- **Headless browsing** — `Bun.WebView` over Puppeteer (1.3.12)
- **Markdown** — `Bun.markdown` over `marked` (1.3.8)
- **Scheduling** — `Bun.cron()` over `node-cron` (1.3.11)
- **Running scripts concurrently** — `bun run --parallel` over `npm-run-all` and `concurrently` (1.3.9)
- **Config formats** — `Bun.TOML`, `Bun.YAML`, `Bun.JSON5` (1.3.7), `Bun.JSONC` (1.3.6), `Bun.XML` (1.4.0) over
`@iarna/toml`, `js-yaml`, `json5`, `jsonc-parser`, and `fast-xml-parser`
- **Newline-delimited JSON** — `Bun.JSONL` over `ndjson` (1.3.7)
- **Tarballs** — `Bun.Archive` over `tar` (1.3.6)
- **Terminal string width** — `Bun.stringWidth` (1.3.5), `Bun.wrapAnsi` (1.3.7), `Bun.sliceAnsi` (1.3.11) over
`string-width`, `wrap-ansi`, `slice-ansi`, and `cli-truncate`
- **Route patterns** — `URLPattern` over `path-to-regexp` (1.3.4)
- **Credential storage** — `Bun.secrets` over a plaintext dotfile, for local development tools
- **Sleeping, hashing, comparison, compression** — `Bun.sleep`, `Bun.hash`, `Bun.CryptoHasher`, `Bun.deepEquals`,
`Bun.gzipSync`, `Bun.zstdCompressSync`
Two boundaries hold in the other direction. **Web Streams are the native currency** — `Bun.file().stream()`,
`Response.body`, and `proc.stdout` are all `ReadableStream`, so `node:stream` is for code shaped around Node, not the
default. **`crypto.randomUUID()` is the standard v4 UUID**; `Bun.randomUUIDv7()` is the time-ordered variant, not a
replacement for it.
## HTTP Server
- **Declare routes in the `routes` object**, and keep `fetch` for genuinely unmatched requests. Precedence is exact,
then parameterized, then wildcard, then the global catch-all — not source order.
- **Never deploy without either an `error` handler or an explicit `development: false`.** `development` defaults to
`process.env.NODE_ENV !== "production"`, so a server whose environment does not set `NODE_ENV=production` renders
Bun's contextual error page to any client that can trigger a throw. On 1.3.14 that page is a ~67 KB HTML overlay
carrying the thrown error's payload in an embedded `__bunfallback` script.
- **An `error` handler decides the response body in both modes** and replaces the error page when `development` is on.
Without one, `development: false` answers 500 with the body `Something went wrong!` (measured on 1.3.14). The stack
trace reaches stderr either way, so a missing handler costs you the response, never the log.
- **`idleTimeout` is 10 seconds and it covers a request whose handler has not yet written a byte.** A slow handler, a
long poll, or a server-sent-events stream is killed mid-response and the client sees a connection reset. Raise
`idleTimeout`, or call `server.timeout(req, 0)` for the one request that needs it.
- **Return a `Response` instance as the route value for anything fixed** — health checks, redirects, constant JSON. Bun
dispatches it with no allocation and caches it for the server's lifetime, so `server.reload()` is the only way to
change it.
- **`req.cookies` writes are applied to the response only under `routes`.** Inside a bare `fetch` handler nothing is
tracked and you build `Set-Cookie` yourself.
- **`server.upgrade(req)` must be followed by returning `undefined`**, never a `Response`.
- **Treat the return of `send()` and `publish()` as a status, not a byte count.** `-1` is backpressure and `0` is
dropped; from 1.4.0 `publish()` also returns `0` when the topic has no subscribers.
- **WebSocket handlers are declared once per server**, in the `websocket` object, not per socket. Type `ws.data` through
the handler object's `data` property.
- **`Response.error()` and any status outside 100–999 route to `error()`** and answer 500 (1.4.0). Earlier releases
wrote an invalid status line.
Read [`${CLAUDE_SKILL_DIR}/references/http-server.md`] when serving files or a directory, wiring WebSocket pub/sub,
tuning server lifecycle or timeouts, or enabling HTTP/2 or HTTP/3 — it carries the five route value shapes and their
caching and `404` behavior, the directory-route path rules, the full `websocket` option set with its defaults, and the
`stop`, `reload`, and `closeIdleConnections` semantics.
## Files
- **`Bun.file(path)` is lazy.** Constructing it touches no disk; `size` is `0` and `exists()` is `false` for a missing
path rather than throwing.
- **`Bun.write(dest, data)` takes anything**: string, `Blob`, `BunFile`, `ArrayBuffer`, `TypedArray`, or `Response`. It
picks the fastest syscall for the pair, so copying a file is `Bun.write(Bun.file(dst), Bun.file(src))`, not a manual
read-then-write.
- **`file.writer()` returns a `FileSink` for incremental writes**, and the process stays alive until `.end()` is called.
`.unref()` opts out.
- **`Bun.stdin`, `Bun.stdout`, and `Bun.stderr` are `BunFile`s**, so streaming a file to stdout is one `Bun.write` call.
## Shell and Processes
- **Interpolated values in `Bun.$` are single literal strings**, so command injection is closed by construction. Two
holes remain and neither is Bun's to close: spawning `bash -c "...${input}"` hands control to a real shell, and an
external command may read attacker-supplied text as its own flag (`--upload-pack=`). Validate arguments you pass on.
- **Only glob patterns written in the template expand** (1.4.0). A `*` arriving through `${...}`, a shell variable,
command substitution, or quoted text is literal, and `?`, `[...]`, and a leading `!` are literal everywhere.
- **A non-zero exit throws `ShellError`.** Use `.nothrow()` where you check `exitCode` yourself.
- **`Bun.$` is not `/bin/sh`.** It is an in-process interpreter, so `.sh` files run identically on Windows and the
builtin set is fixed.
- **`stderr` defaults to `"inherit"` in `Bun.spawn`, so `proc.stderr` is `undefined`.** Pass `stderr: "pipe"` to read
it. `stdout` defaults to `"pipe"` and `stdin` to `null`.
- **The parent process does not exit while a child is alive.** Call `proc.unref()` for a child that must not hold it
open.
- **Set `serialization: "json"` for IPC with a Node process.** The default `"advanced"` is JavaScriptCore's format and
Node cannot read it.
Read [`${CLAUDE_SKILL_DIR}/references/shell-and-processes.md`] when redirecting shell I/O into JavaScript objects,
driving an interactive program through a PTY, applying cgroup limits, or spawning a `Worker` — it carries the
redirection and builtin tables, the stdio option matrix, the `Bun.Terminal` platform differences, and the worker
options.
## Testing
- **Import from `bun:test`**, never from `jest` or `vitest`. `jest.*` and `vi.*` exist inside `bun:test` for ported
suites.
- **`mock.module()` is not hoisted.** Unlike `jest.mock`, it runs where it is written, so a module imported above it has
already been evaluated and its side effects have already happened. Put the override in a `--preload` script when the
original must never run.
- **Pass the dependency in before reaching for `mock.module()`.** A fake passed as a parameter or a constructor argument
replaces one collaborator for one test. A module override reaches every importer, even one that imported the module
before the override, and `mock.restore()` does not undo it.
- **Pick the right global reset.** `mock.clearAllMocks()` clears history and keeps implementations;
`jest.resetAllMocks()` also drops them (1.4.0 — before, it behaved like `clearAllMocks`); `mock.restore()` restores
spied originals and undoes no `mock.module()` override.
- **`expectTypeOf` is a no-op at runtime.** A green `bun test` proves nothing about types — run the project's type-check
command separately.
- **`bun test` sets `NODE_ENV=test` unless it is already set, and runs in UTC.** `process.env.TZ` stays unset — the
runtime zone is UTC, not an environment variable a preload can read.
- **An unhandled rejection between tests fails the run.** When it happens while a file loads, none of that file's tests
run at all.
- **Three concurrency knobs are independent**: `--parallel` spreads files across processes, `--concurrent` and
`test.concurrent` overlap async tests inside one file, `--shard=i/n` splits files across machines (`--parallel` and
`--shard` from 1.3.13). `--parallel` implies `--isolate`.
- **Reach for `--parallel --no-isolate` on a suite of many small files sharing a large import graph.** Isolation
re-evaluates every import per file, and on that shape plain `bun test` beats isolated workers.
- **Key per-worker resources off `BUN_TEST_WORKER_ID`** (or `JEST_WORKER_ID`) so parallel workers do not share a
database or a port.
- **The default per-test timeout is 5000 ms.**
Read [`${CLAUDE_SKILL_DIR}/references/testing.md`] when writing the first test in a project, configuring CI sharding,
reaching for a matcher or a snapshot form, or debugging a mock that stopped returning values — it carries the discovery
patterns, the matcher inventory, the mock-reset semantics in full, the `--timings` sharding workflow, and the
`bunfig.toml` test keys.
## Package Manager
- **`trustedDependencies` replaces Bun's built-in allow list; it never extends it.** Adding one package silently
disables `postinstall` for every package the built-in list covered, so re-list the ones you still need. `[]` opts out
of lifecycle scripts entirely.
View on GitHub