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 BunFiles, 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.
- 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 bunx tsc --noEmit
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.
- The built-in list applies only to npm-registry sources (1.3.5). A
file:, link:, git:, or github:
dependency named esbuild inherits nothing from the real esbuild.
- The linker default comes from the lockfile's
configVersion, not from a global setting. A new monorepo gets the
isolated linker (1.3.2); a new single-package project and every project whose lockfile predates 1.3.2 get the hoisted
one. Two machines therefore lay out node_modules differently unless bun.lock is committed.
- The isolated linker still permits phantom dependencies by default. Set
[install] hoist = false to stop
node_modules/.bun/node_modules from being created.
- Run
bun ci in CI, not bun install. Bun does not turn on the frozen lockfile automatically; bun ci is
bun install --frozen-lockfile.
--production implies --frozen-lockfile and skips devDependencies, but removes nothing already installed.
bun prune --production (1.4.0) is what shrinks a build image.
Read [${CLAUDE_SKILL_DIR}/references/package-manager.md] when configuring a monorepo, choosing a linker, hardening a
supply chain, or diagnosing an install that differs between machines — it carries the configVersion table, the
trustedDependencies modes, workspaces and catalogs, the --filter grammar, minimumReleaseAge and the security
scanner, and the bunfig.toml install keys.
Building and Compiling
Bun.build() rejects with an AggregateError on failure. throw defaults to true, so result.success is
meaningful only when you pass throw: false. Checking success on a default build reads a value that never arrives.
target defaults to "browser". Set target: "bun" for server code, which also emits the // @bun pragma so
the runtime skips re-transpiling.
- Ship
--compile --minify --sourcemap --bytecode for a production binary. Bytecode needs target: "bun", and for
ES modules it needs --compile as well (1.3.9).
- A compiled binary does not auto-load
tsconfig.json or package.json (1.3.4), but it does auto-load .env and
bunfig.toml. Pass --no-compile-autoload-dotenv and --no-compile-autoload-bunfig where a deployment must not read
files from its working directory.
- Cross-compile with
--target bun-<os>-<arch>[-musl]. From 1.4.0 x64 ships only the baseline build and the
-march=haswell build is gone, so -baseline and -modern resolve to the same binary and neither is a choice.
- A macro's return value must be serializable and its arguments statically known. Code inside
node_modules cannot
invoke a macro, though your own code may import one from a package.
Bun.markdown.html() does not sanitize. Raw HTML, event-handler attributes, and javascript: hrefs pass through
verbatim, so untrusted Markdown needs a sanitizer before it reaches a browser.
Read [${CLAUDE_SKILL_DIR}/references/build-and-compile.md] when configuring a bundle, embedding assets in an
executable, writing a plugin, or reaching for a macro — it carries the full Bun.build option set, code-splitting and
optimizeImports behavior, the cross-compilation targets, the embedded-file and /$bunfs rules, the plugin hooks, and
the macro serialization constraints.
Data Clients
- Open
bun:sqlite with { strict: true }. Without it a missing bind parameter is not an error — a typo binds
nothing and the query returns a wrong result silently.
db.query() caches the compiled statement (20 most recent); db.prepare() does not. Use prepare() for SQL
generated at runtime so one-off queries do not evict the statements that repeat.
- Enable WAL with
PRAGMA journal_mode = WAL for anything with concurrent readers.
- Set
safeIntegers: true when a column can exceed 2^53. The default returns number and rounds silently.
- Every interpolation in a
Bun.sql template is a parameter. Use the sql() helper for identifiers, column
subsets, and value lists; sql.unsafe escapes nothing.
Bun.sql queries are lazy — they start on await or .execute(), and .cancel() stops a running one.
- Release a reserved connection.
sql.reserve() takes one out of the pool until release(), or use using.
- Set
prepare: false behind PgBouncer in transaction mode.
Read [${CLAUDE_SKILL_DIR}/references/data-clients.md] when choosing a client, closing a SQLite database cleanly,
building dynamic SQL, or configuring pooling and TLS — it carries the bun:sqlite close and WAL-sidecar semantics, the
Bun.sql fragment helpers and query modes, and the Bun.redis and Bun.S3Client configuration.
Configuration and Environment
bunfig.toml configures the runtime, bun test, bun install, and bun run. A CLI flag overrides it; a
project's bunfig.toml overrides .npmrc for the same key (1.4.0).
.env files load automatically, in order: .env, then .env.{development,production,test} by NODE_ENV, then
.env.local (skipped when NODE_ENV=test), then .env.{mode}.local. Values expand $VAR references unless the $
is escaped.
- Bun invoked as
node does not load .env (1.4.0) — under bun --bun, bunx --bun, or a node symlink. A
package.json script calling node under bun --bun run therefore sees those variables as undefined. Pass
--env-file to keep them.
- Disable automatic loading in production with
--no-env-file or env = false. Files named by --env-file still
load.
bunfig.toml parsing is strict (1.4.0): an unquoted string value, a missing newline between pairs, or an integer
past Number.MAX_SAFE_INTEGER is a SyntaxError at startup.
Bun.YAML follows YAML 1.2 (1.3.5), so yes, no, on, and off parse as strings. An on: key in a GitHub
Actions workflow is the string "on".
Node Compatibility
From 1.4.0 Bun targets Node 26 and reports itself as such; 1.3.x reports Node 24. Most packages run unchanged. Reach for
a node: module deliberately in these cases:
node:sea — not implemented. bun build --compile is the replacement and a different API.
node:test — partial. Write bun:test.
node:crypto beyond BoringSSL — no ed448, x448, rsa-pss, dsa, dh, secp256k1, or the CCM, OCB, XTS,
and chacha20-poly1305 ciphers.
node:async_hooks — AsyncLocalStorage works but does not propagate into Worker, MessagePort, or
BroadcastChannel. createHook and the async-id APIs are stubs.
- HTTP load balancing across processes —
node:cluster shares an HTTP socket only on Linux, through SO_REUSEPORT.
node:v8 serialize/deserialize — JavaScriptCore's wire format, not V8's, so a buffer does not travel between
Bun and Node.
- Loader hooks —
module.register is a no-op. Use Bun.plugin.
Read [${CLAUDE_SKILL_DIR}/references/node-compat.md] when porting a Node codebase, when a node: API behaves
differently under Bun, or when a package that works under Node fails — it carries the per-module gap list and the
fetch, Web API, and module-resolution behavior changes in 1.4.0 that break working code.
Application
When writing Bun, apply these conventions silently — do not narrate a rule while following it. Where existing code
uses a node: API that has a Bun-native equivalent, follow the codebase. Flag the alternative once.
When reviewing Bun, cite the violation and show the fix inline. Do not lecture.
Bad: "Bun's build API generally prefers rejection over a status flag, so you may want to..."
Good: if (!result.success) -> Bun.build() rejects; catch the AggregateError, or pass throw: false
Integration
The javascript and typescript skills own the language and the type system; this skill states only where Bun
departs from them. The nodejs skill owns what a node: module does — this skill owns only where Bun's
implementation is absent, partial, or behaves differently, and never teaches Node itself.
bun:test and bun install are this skill's, and the vitest skill owns Vitest. Which of them a project adopts is a
project decision, not a rule either skill states. The coding skill governs workflow.
1---2name: bun3description: 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.4---56Bun's value is that the runtime, the toolchain, and the batteries are one binary. Two biases decide most calls:78- **Reach for what already ships.** A dependency earns its place by doing what `Bun.*`, `bun:*`, and the bundled9 toolchain do not. Adding one that duplicates a built-in is a defect.10- **Write `node:` only where Bun has no native equivalent, or where the code must also run under Node.** Portability is11 a requirement to state, not a habit to keep.1213## Release Version1415Bun ships one stable line and no long-lived support branches, so `bun upgrade` moves a machine to whatever release is16newest. Pin the version in CI (`oven-sh/setup-bun`, `bun-version`) so a developer machine and a runner agree, and state17the release a feature needs in the sentence that uses it. Never write "the latest version supports X".1819The rules below assume 1.3.0 or later unless a version anchor says otherwise. Floors for the features these rules20reference:2122- **1.3.0** — `Bun.sql` for MySQL, MariaDB, and SQLite; `Bun.redis`; `Bun.secrets`; `Bun.YAML`; catalogs;23 `test.concurrent`, `test.serial`, and `--concurrent`; `--randomize`; `expectTypeOf`24- **1.3.2** — the isolated linker as the default for new monorepos; `nativeDependencies`; `ignoreScripts`25- **1.3.3** — `test(..., { retry })`26- **1.3.4** — `jest.useFakeTimers()`; `URLPattern`; `--compile` stops auto-loading `tsconfig.json` and `package.json`27- **1.3.5** — `Bun.Terminal`; `Bun.stringWidth`; `bun:bundle` feature flags; `trustedDependencies` limited to npm28 sources29- **1.3.6** — `Bun.JSONC`; `Bun.Archive`; `Bun.build({ files })`; `metafile: true`30- **1.3.7** — `Bun.JSON5`; `Bun.JSONL`; `Bun.wrapAnsi`31- **1.3.8** — `Bun.markdown`; `--metafile-md`32- **1.3.9** — `bun run --parallel`; bytecode for ES modules33- **1.3.10** — `optimizeImports`; standard TC39 decorators; `--compile --target=browser` for a single HTML file34- **1.3.11** — `Bun.cron()`; `Bun.sliceAnsi`35- **1.3.12** — `Bun.WebView`36- **1.3.13** — `bun test --parallel`, `--isolate`, `--shard`, `--changed`; `Range` and conditional requests in37 `Bun.serve`38- **1.3.14** — `Bun.Image`; the global virtual store; HTTP/3 in `Bun.serve`; HTTP/2 and HTTP/3 in `fetch()`39- **1.4.0** — Node 26 compatibility; `Bun.XML`; `Bun.isStandaloneExecutable`; `Bun.spawn({ cgroup })`; directory routes;40 `bun audit fix`, `bun dedupe`, `bun prune`, `bun pm diff`, `bun pm licenses`; `bun test --timings`; `--asset`;41 `lockfileVersion: 2`4243## Prefer the Built-in4445Reach for the built-in, and drop the package it replaces from `package.json`.4647- **HTTP server** — `Bun.serve()` over `http.createServer()`, Express, or Fastify48- **Files** — `Bun.file()` and `Bun.write()` over `node:fs` read and write helpers49- **Directories** — `node:fs`. There is no `Bun.*` equivalent for `mkdir` or `readdir`50- **Globbing** — `Bun.Glob` over `glob` and `fast-glob`51- **Shelling out** — `Bun.$` over `child_process.exec`, `zx`, `execa`, `cross-env`, and `rimraf`52- **Processes** — `Bun.spawn` and `Bun.spawnSync` over `child_process.spawn`53- **Pseudo-terminals** — `Bun.Terminal` over `node-pty` (1.3.5)54- **SQLite** — `bun:sqlite` over `better-sqlite3`55- **Postgres, MySQL, MariaDB** — `Bun.sql` over `pg` and `mysql2` (1.3.0)56- **Redis and Valkey** — `Bun.redis` over `ioredis` (1.3.0)57- **S3-compatible storage** — `Bun.S3Client` over `@aws-sdk/client-s3`58- **Password hashing** — `Bun.password` over `bcrypt` and `argon2`59- **Testing** — `bun:test` over Jest60- **Bundling** — `Bun.build()` and `bun build` over webpack, Rollup, and esbuild61- **Images** — `Bun.Image` over `sharp` (1.3.14)62- **Headless browsing** — `Bun.WebView` over Puppeteer (1.3.12)63- **Markdown** — `Bun.markdown` over `marked` (1.3.8)64- **Scheduling** — `Bun.cron()` over `node-cron` (1.3.11)65- **Running scripts concurrently** — `bun run --parallel` over `npm-run-all` and `concurrently` (1.3.9)66- **Config formats** — `Bun.TOML`, `Bun.YAML`, `Bun.JSON5` (1.3.7), `Bun.JSONC` (1.3.6), `Bun.XML` (1.4.0) over67 `@iarna/toml`, `js-yaml`, `json5`, `jsonc-parser`, and `fast-xml-parser`68- **Newline-delimited JSON** — `Bun.JSONL` over `ndjson` (1.3.7)69- **Tarballs** — `Bun.Archive` over `tar` (1.3.6)70- **Terminal string width** — `Bun.stringWidth` (1.3.5), `Bun.wrapAnsi` (1.3.7), `Bun.sliceAnsi` (1.3.11) over71 `string-width`, `wrap-ansi`, `slice-ansi`, and `cli-truncate`72- **Route patterns** — `URLPattern` over `path-to-regexp` (1.3.4)73- **Credential storage** — `Bun.secrets` over a plaintext dotfile, for local development tools74- **Sleeping, hashing, comparison, compression** — `Bun.sleep`, `Bun.hash`, `Bun.CryptoHasher`, `Bun.deepEquals`,75 `Bun.gzipSync`, `Bun.zstdCompressSync`7677Two boundaries hold in the other direction. **Web Streams are the native currency** — `Bun.file().stream()`,78`Response.body`, and `proc.stdout` are all `ReadableStream`, so `node:stream` is for code shaped around Node, not the79default. **`crypto.randomUUID()` is the standard v4 UUID**; `Bun.randomUUIDv7()` is the time-ordered variant, not a80replacement for it.8182## HTTP Server8384- **Declare routes in the `routes` object**, and keep `fetch` for genuinely unmatched requests. Precedence is exact,85 then parameterized, then wildcard, then the global catch-all — not source order.86- **Never deploy without either an `error` handler or an explicit `development: false`.** `development` defaults to87 `process.env.NODE_ENV !== "production"`, so a server whose environment does not set `NODE_ENV=production` renders88 Bun's contextual error page to any client that can trigger a throw. On 1.3.14 that page is a ~67 KB HTML overlay89 carrying the thrown error's payload in an embedded `__bunfallback` script.90- **An `error` handler decides the response body in both modes** and replaces the error page when `development` is on.91 Without one, `development: false` answers 500 with the body `Something went wrong!` (measured on 1.3.14). The stack92 trace reaches stderr either way, so a missing handler costs you the response, never the log.93- **`idleTimeout` is 10 seconds and it covers a request whose handler has not yet written a byte.** A slow handler, a94 long poll, or a server-sent-events stream is killed mid-response and the client sees a connection reset. Raise95 `idleTimeout`, or call `server.timeout(req, 0)` for the one request that needs it.96- **Return a `Response` instance as the route value for anything fixed** — health checks, redirects, constant JSON. Bun97 dispatches it with no allocation and caches it for the server's lifetime, so `server.reload()` is the only way to98 change it.99- **`req.cookies` writes are applied to the response only under `routes`.** Inside a bare `fetch` handler nothing is100 tracked and you build `Set-Cookie` yourself.101- **`server.upgrade(req)` must be followed by returning `undefined`**, never a `Response`.102- **Treat the return of `send()` and `publish()` as a status, not a byte count.** `-1` is backpressure and `0` is103 dropped; from 1.4.0 `publish()` also returns `0` when the topic has no subscribers.104- **WebSocket handlers are declared once per server**, in the `websocket` object, not per socket. Type `ws.data` through105 the handler object's `data` property.106- **`Response.error()` and any status outside 100–999 route to `error()`** and answer 500 (1.4.0). Earlier releases107 wrote an invalid status line.108109Read [`${CLAUDE_SKILL_DIR}/references/http-server.md`] when serving files or a directory, wiring WebSocket pub/sub,110tuning server lifecycle or timeouts, or enabling HTTP/2 or HTTP/3 — it carries the five route value shapes and their111caching and `404` behavior, the directory-route path rules, the full `websocket` option set with its defaults, and the112`stop`, `reload`, and `closeIdleConnections` semantics.113114## Files115116- **`Bun.file(path)` is lazy.** Constructing it touches no disk; `size` is `0` and `exists()` is `false` for a missing117 path rather than throwing.118- **`Bun.write(dest, data)` takes anything**: string, `Blob`, `BunFile`, `ArrayBuffer`, `TypedArray`, or `Response`. It119 picks the fastest syscall for the pair, so copying a file is `Bun.write(Bun.file(dst), Bun.file(src))`, not a manual120 read-then-write.121- **`file.writer()` returns a `FileSink` for incremental writes**, and the process stays alive until `.end()` is called.122 `.unref()` opts out.123- **`Bun.stdin`, `Bun.stdout`, and `Bun.stderr` are `BunFile`s**, so streaming a file to stdout is one `Bun.write` call.124125## Shell and Processes126127- **Interpolated values in `Bun.$` are single literal strings**, so command injection is closed by construction. Two128 holes remain and neither is Bun's to close: spawning `bash -c "...${input}"` hands control to a real shell, and an129 external command may read attacker-supplied text as its own flag (`--upload-pack=`). Validate arguments you pass on.130- **Only glob patterns written in the template expand** (1.4.0). A `*` arriving through `${...}`, a shell variable,131 command substitution, or quoted text is literal, and `?`, `[...]`, and a leading `!` are literal everywhere.132- **A non-zero exit throws `ShellError`.** Use `.nothrow()` where you check `exitCode` yourself.133- **`Bun.$` is not `/bin/sh`.** It is an in-process interpreter, so `.sh` files run identically on Windows and the134 builtin set is fixed.135- **`stderr` defaults to `"inherit"` in `Bun.spawn`, so `proc.stderr` is `undefined`.** Pass `stderr: "pipe"` to read136 it. `stdout` defaults to `"pipe"` and `stdin` to `null`.137- **The parent process does not exit while a child is alive.** Call `proc.unref()` for a child that must not hold it138 open.139- **Set `serialization: "json"` for IPC with a Node process.** The default `"advanced"` is JavaScriptCore's format and140 Node cannot read it.141142Read [`${CLAUDE_SKILL_DIR}/references/shell-and-processes.md`] when redirecting shell I/O into JavaScript objects,143driving an interactive program through a PTY, applying cgroup limits, or spawning a `Worker` — it carries the144redirection and builtin tables, the stdio option matrix, the `Bun.Terminal` platform differences, and the worker145options.146147## Testing148149- **Import from `bun:test`**, never from `jest` or `vitest`. `jest.*` and `vi.*` exist inside `bun:test` for ported150 suites.151- **`mock.module()` is not hoisted.** Unlike `jest.mock`, it runs where it is written, so a module imported above it has152 already been evaluated and its side effects have already happened. Put the override in a `--preload` script when the153 original must never run.154- **Pick the right global reset.** `mock.clearAllMocks()` clears history and keeps implementations;155 `jest.resetAllMocks()` also drops them (1.4.0 — before, it behaved like `clearAllMocks`); `mock.restore()` restores156 spied originals and undoes no `mock.module()` override.157- **`expectTypeOf` is a no-op at runtime.** A green `bun test` proves nothing about types — run `bunx tsc --noEmit`158 separately.159- **`bun test` sets `NODE_ENV=test` unless it is already set, and runs in UTC.** `process.env.TZ` stays unset — the160 runtime zone is UTC, not an environment variable a preload can read.161- **An unhandled rejection between tests fails the run.** When it happens while a file loads, none of that file's tests162 run at all.163- **Three concurrency knobs are independent**: `--parallel` spreads files across processes, `--concurrent` and164 `test.concurrent` overlap async tests inside one file, `--shard=i/n` splits files across machines (`--parallel` and165 `--shard` from 1.3.13). `--parallel` implies `--isolate`.166- **Reach for `--parallel --no-isolate` on a suite of many small files sharing a large import graph.** Isolation167 re-evaluates every import per file, and on that shape plain `bun test` beats isolated workers.168- **Key per-worker resources off `BUN_TEST_WORKER_ID`** (or `JEST_WORKER_ID`) so parallel workers do not share a169 database or a port.170- **The default per-test timeout is 5000 ms.**171172Read [`${CLAUDE_SKILL_DIR}/references/testing.md`] when writing the first test in a project, configuring CI sharding,173reaching for a matcher or a snapshot form, or debugging a mock that stopped returning values — it carries the discovery174patterns, the matcher inventory, the mock-reset semantics in full, the `--timings` sharding workflow, and the175`bunfig.toml` test keys.176177## Package Manager178179- **`trustedDependencies` replaces Bun's built-in allow list; it never extends it.** Adding one package silently180 disables `postinstall` for every package the built-in list covered, so re-list the ones you still need. `[]` opts out181 of lifecycle scripts entirely.182- **The built-in list applies only to npm-registry sources** (1.3.5). A `file:`, `link:`, `git:`, or `github:`183 dependency named `esbuild` inherits nothing from the real `esbuild`.184- **The linker default comes from the lockfile's `configVersion`, not from a global setting.** A new monorepo gets the185 isolated linker (1.3.2); a new single-package project and every project whose lockfile predates 1.3.2 get the hoisted186 one. Two machines therefore lay out `node_modules` differently unless `bun.lock` is committed.187- **The isolated linker still permits phantom dependencies by default.** Set `[install] hoist = false` to stop188 `node_modules/.bun/node_modules` from being created.189- **Run `bun ci` in CI, not `bun install`.** Bun does not turn on the frozen lockfile automatically; `bun ci` is190 `bun install --frozen-lockfile`.191- **`--production` implies `--frozen-lockfile` and skips `devDependencies`, but removes nothing already installed.**192 `bun prune --production` (1.4.0) is what shrinks a build image.193194Read [`${CLAUDE_SKILL_DIR}/references/package-manager.md`] when configuring a monorepo, choosing a linker, hardening a195supply chain, or diagnosing an install that differs between machines — it carries the `configVersion` table, the196`trustedDependencies` modes, workspaces and catalogs, the `--filter` grammar, `minimumReleaseAge` and the security197scanner, and the `bunfig.toml` install keys.198199## Building and Compiling200201- **`Bun.build()` rejects with an `AggregateError` on failure.** `throw` defaults to `true`, so `result.success` is202 meaningful only when you pass `throw: false`. Checking `success` on a default build reads a value that never arrives.203- **`target` defaults to `"browser"`.** Set `target: "bun"` for server code, which also emits the `// @bun` pragma so204 the runtime skips re-transpiling.205- **Ship `--compile --minify --sourcemap --bytecode` for a production binary.** Bytecode needs `target: "bun"`, and for206 ES modules it needs `--compile` as well (1.3.9).207- **A compiled binary does not auto-load `tsconfig.json` or `package.json`** (1.3.4), but it does auto-load `.env` and208 `bunfig.toml`. Pass `--no-compile-autoload-dotenv` and `--no-compile-autoload-bunfig` where a deployment must not read209 files from its working directory.210- **Cross-compile with `--target bun-<os>-<arch>[-musl]`.** From 1.4.0 x64 ships only the baseline build and the211 `-march=haswell` build is gone, so `-baseline` and `-modern` resolve to the same binary and neither is a choice.212- **A macro's return value must be serializable and its arguments statically known.** Code inside `node_modules` cannot213 invoke a macro, though your own code may import one from a package.214- **`Bun.markdown.html()` does not sanitize.** Raw HTML, event-handler attributes, and `javascript:` hrefs pass through215 verbatim, so untrusted Markdown needs a sanitizer before it reaches a browser.216217Read [`${CLAUDE_SKILL_DIR}/references/build-and-compile.md`] when configuring a bundle, embedding assets in an218executable, writing a plugin, or reaching for a macro — it carries the full `Bun.build` option set, code-splitting and219`optimizeImports` behavior, the cross-compilation targets, the embedded-file and `/$bunfs` rules, the plugin hooks, and220the macro serialization constraints.221222## Data Clients223224- **Open `bun:sqlite` with `{ strict: true }`.** Without it a missing bind parameter is not an error — a typo binds225 nothing and the query returns a wrong result silently.226- **`db.query()` caches the compiled statement (20 most recent); `db.prepare()` does not.** Use `prepare()` for SQL227 generated at runtime so one-off queries do not evict the statements that repeat.228- **Enable WAL** with `PRAGMA journal_mode = WAL` for anything with concurrent readers.229- **Set `safeIntegers: true` when a column can exceed 2^53.** The default returns `number` and rounds silently.230- **Every interpolation in a `Bun.sql` template is a parameter.** Use the `sql()` helper for identifiers, column231 subsets, and value lists; `sql.unsafe` escapes nothing.232- **`Bun.sql` queries are lazy** — they start on `await` or `.execute()`, and `.cancel()` stops a running one.233- **Release a reserved connection.** `sql.reserve()` takes one out of the pool until `release()`, or use `using`.234- **Set `prepare: false` behind PgBouncer in transaction mode.**235236Read [`${CLAUDE_SKILL_DIR}/references/data-clients.md`] when choosing a client, closing a SQLite database cleanly,237building dynamic SQL, or configuring pooling and TLS — it carries the `bun:sqlite` close and WAL-sidecar semantics, the238`Bun.sql` fragment helpers and query modes, and the `Bun.redis` and `Bun.S3Client` configuration.239240## Configuration and Environment241242- **`bunfig.toml` configures the runtime, `bun test`, `bun install`, and `bun run`.** A CLI flag overrides it; a243 project's `bunfig.toml` overrides `.npmrc` for the same key (1.4.0).244- **`.env` files load automatically**, in order: `.env`, then `.env.{development,production,test}` by `NODE_ENV`, then245 `.env.local` (skipped when `NODE_ENV=test`), then `.env.{mode}.local`. Values expand `$VAR` references unless the `$`246 is escaped.247- **Bun invoked as `node` does not load `.env`** (1.4.0) — under `bun --bun`, `bunx --bun`, or a `node` symlink. A248 `package.json` script calling `node` under `bun --bun run` therefore sees those variables as `undefined`. Pass249 `--env-file` to keep them.250- **Disable automatic loading in production with `--no-env-file` or `env = false`.** Files named by `--env-file` still251 load.252- **`bunfig.toml` parsing is strict** (1.4.0): an unquoted string value, a missing newline between pairs, or an integer253 past `Number.MAX_SAFE_INTEGER` is a `SyntaxError` at startup.254- **`Bun.YAML` follows YAML 1.2** (1.3.5), so `yes`, `no`, `on`, and `off` parse as strings. An `on:` key in a GitHub255 Actions workflow is the string `"on"`.256257## Node Compatibility258259From 1.4.0 Bun targets Node 26 and reports itself as such; 1.3.x reports Node 24. Most packages run unchanged. Reach for260a `node:` module deliberately in these cases:261262- **`node:sea`** — not implemented. `bun build --compile` is the replacement and a different API.263- **`node:test`** — partial. Write `bun:test`.264- **`node:crypto` beyond BoringSSL** — no `ed448`, `x448`, `rsa-pss`, `dsa`, `dh`, `secp256k1`, or the CCM, OCB, XTS,265 and `chacha20-poly1305` ciphers.266- **`node:async_hooks`** — `AsyncLocalStorage` works but does not propagate into `Worker`, `MessagePort`, or267 `BroadcastChannel`. `createHook` and the async-id APIs are stubs.268- **HTTP load balancing across processes** — `node:cluster` shares an HTTP socket only on Linux, through `SO_REUSEPORT`.269- **`node:v8` `serialize`/`deserialize`** — JavaScriptCore's wire format, not V8's, so a buffer does not travel between270 Bun and Node.271- **Loader hooks** — `module.register` is a no-op. Use `Bun.plugin`.272273Read [`${CLAUDE_SKILL_DIR}/references/node-compat.md`] when porting a Node codebase, when a `node:` API behaves274differently under Bun, or when a package that works under Node fails — it carries the per-module gap list and the275`fetch`, Web API, and module-resolution behavior changes in 1.4.0 that break working code.276277## Application278279When **writing** Bun, apply these conventions silently — do not narrate a rule while following it. Where existing code280uses a `node:` API that has a Bun-native equivalent, follow the codebase. Flag the alternative once.281282When **reviewing** Bun, cite the violation and show the fix inline. Do not lecture.283284```285Bad: "Bun's build API generally prefers rejection over a status flag, so you may want to..."286Good: if (!result.success) -> Bun.build() rejects; catch the AggregateError, or pass throw: false287```288289## Integration290291The **javascript** and **typescript** skills own the language and the type system; this skill states only where Bun292departs from them. The **nodejs** skill owns what a `node:` module does — this skill owns only where Bun's293implementation is absent, partial, or behaves differently, and never teaches Node itself.294295`bun:test` and `bun install` are this skill's, and the **vitest** skill owns Vitest. Which of them a project adopts is a296project decision, not a rule either skill states. The **coding** skill governs workflow.