Bun Runtime skill for Claude Code
Adopt Bun one role at a time, with proof. Claude treats Bun as four separate tools and adopts the low-risk ones first: package manager, then test runner, then bundler, then runtime. It commits the text lockfile, freezes it in CI, audits blocked install scripts, writes Bun.serve handlers with graceful shutdown and raised idle timeouts, and proves Node compatibility with the real suite and a staging image before anything ships.
- name
bun-runtime - size223 lines · ≈2,200 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Adopts and operates Bun as a package manager, test runner, bundler, or full runtime - bun install with the text bun.lock and trustedDependencies, Bun.serve routes and websockets, built-in SQL, Redis, S3 and SQLite clients, bun test with mocks and coverage, bun build and single-file executables, Docker images, and finding Node.js compatibility gaps before production. Use when migrating a Node project to Bun, starting a Bun API, replacing Jest with bun test, or debugging code that works in Node but breaks under Bun. Try asking
- Switch this Node repo to bun install and bun test without changing production
- Write a small Bun.serve API with Postgres and graceful shutdown
- This package works on Node but crashes under Bun. Figure out why
Use it when
- Migrating a Node project's installs or tests to Bun
- Starting a new API or CLI on the Bun runtime
- Building single-file executables with bun build --compile
- Code behaves differently under Bun than under Node
Skip it when
- The app targets Deno or its permission model — use deno-runtime
- You are building a Hono app meant to run on several runtimes — use hono-edge
- The service stays on Node with Express — use express-node
How to check the result
A clean CI checkout passes bun install --frozen-lockfile, the test suite and tsc, and the pinned production image serves health checks and drains requests on SIGTERM.
The common mistake it prevents
Assuming a package works because bun install succeeded, when its postinstall script was silently blocked because it is not in trustedDependencies.
Done well means: the team adopted only the Bun roles that pay off, the lockfile is committed and frozen in CI, lifecycle scripts run only for trusted packages, the test suite passes under the runtime that serves production, and every Node API the app relies on has been exercised under Bun before deploy.
Choose the roles first
Bun is four tools. Adopt them separately and in this order of risk:
| Role | Risk | Adopt when |
|---|---|---|
Package manager (bun install) | Low | Installs are slow; works with Node as the runtime |
Test runner (bun test) | Low to medium | Jest-style suites, no heavy Jest plugins or custom environments |
Bundler (bun build) | Medium | Server bundles, CLIs, single-file executables; keep Vite for complex frontends |
Runtime (bun run, Bun.serve) | Medium to high | Dependencies verified under Bun; you control the deploy image |
A project can use Bun for installs and tests while production still runs Node. That is a valid end state, not a half-migration.
Workflow
- [ ] 1. Inventory runtime-sensitive dependencies and scripts
- [ ] 2. Switch the package manager, commit bun.lock, freeze it in CI
- [ ] 3. Run the existing test suite under bun test (or keep the old runner)
- [ ] 4. Run the app under Bun locally and in a staging image
- [ ] 5. Replace Node-specific pieces only where Bun has a clear win
- [ ] 6. Build the production image and verify shutdown and health checks
1. Inventory
bun --version
grep -E '"(engines|packageManager)"' package.json
grep -rn "node-gyp\|binding.gyp\|prebuild" node_modules/*/package.json 2>/dev/null | head
grep -rnE "from 'node:(vm|inspector|cluster|v8|worker_threads|http2)'" src | head
Flag: native addons, packages with install scripts, vm/inspector/v8 usage, APM
agents that monkey-patch Node internals, and frameworks with documented Bun caveats.
Check each against the Node.js compatibility page in the Bun docs for the installed Bun
version; compatibility improves every minor release, so verify rather than recall.
2. Package manager
bun install # reads package.json, writes bun.lock (text, Bun 1.2+)
bun add zod # dependency
bun add -d @types/bun typescript # dev dependency
bun install --frozen-lockfile # CI: fail if bun.lock is out of date
bun outdated
bun pm untrusted # packages whose install scripts were blocked
bun pm trust <pkg> # allow one, recorded in trustedDependencies
- Bun does not run dependency lifecycle scripts unless the package is in
trustedDependenciesin package.json (a small default allowlist of popular packages aside). A package that “installs fine but crashes at runtime” often had its postinstall blocked; checkbun pm untrustedbefore adding it to the trust list. - Projects with the old binary
bun.lockb: runbun install --save-text-lockfileonce, commitbun.lock, deletebun.lockb. - Delete other lockfiles (
package-lock.json,pnpm-lock.yaml) in the same commit so tools do not disagree. - Workspaces: set
install.linker("isolated"or"hoisted") explicitly inbunfig.tomlso behavior does not depend on the Bun version’s default.
3. Tests
import { describe, test, expect, mock, spyOn, beforeEach } from "bun:test";
import { priceWithTax } from "./price";
mock.module("./rates", () => ({ taxRate: () => 0.2 }));
describe("priceWithTax", () => {
test("adds tax in cents", () => {
expect(priceWithTax(1000)).toBe(1200);
});
});
# bunfig.toml
[test]
preload = ["./test/setup.ts"]
coverage = true
coverageThreshold = { lines = 0.8, functions = 0.8 }
bun test # discovers *.test.ts, *_test.ts, *.spec.ts
bun test --watch
bun test --coverage --coverage-reporter=lcov
bun test --rerun-each 20 src/flaky.test.ts # flush out order-dependent tests
bun test -t "priceWithTax" # filter by name
- Jest globals (
describe,expect,jest.fn) mostly work; suites leaning on Jest environments, custom transformers, or Vitest-only APIs may not. If the suite already runs on Vitest, keep Vitest and only switch installs. - DOM tests need a preload that registers happy-dom (
@happy-dom/global-registrator). - Recent releases add
--parallel,--shardand--changed(1.3.13+); confirm withbun test --helpbefore using them in CI.
4. Runtime and Bun.serve
import { sql } from "bun"; // Postgres via DATABASE_URL
const server = Bun.serve({
port: Number(process.env.PORT ?? 3000),
idleTimeout: 30, // seconds; default 10 closes slow streams/SSE
routes: {
"/healthz": new Response("ok"),
"/api/orders/:id": {
GET: async (req) => {
const [order] = await sql`select * from orders where id = ${req.params.id}`;
return order ? Response.json(order) : new Response("not found", { status: 404 });
},
},
"/api/orders": {
POST: async (req) => {
const body = await req.json();
// validate body (zod/valibot) before use
return Response.json({ ok: true }, { status: 201 });
},
},
},
fetch() {
return new Response("not found", { status: 404 });
},
error(err) {
console.error(err);
return new Response("internal error", { status: 500 });
},
});
process.on("SIGTERM", async () => {
await server.stop(); // stop accepting, let in-flight requests finish
await sql.close();
process.exit(0);
});
sqltagged templates parameterize values; never build SQL with string concatenation.- Websockets: return
server.upgrade(req, { data })fromfetchand handlewebsocket: { open, message, close }; per-connection data goes inws.data. - Built-ins worth using:
Bun.file/Bun.write,Bun.password.hash(argon2id by default),bun:sqlite,sql(Postgres; MySQL and SQLite adapters in 1.3+),redisclient,s3client, andBun.$for shell scripts. - Bun loads
.env,.env.localand.env.<NODE_ENV>automatically. In containers, pass real environment variables and do not ship.envfiles in the image. - Bun strips TypeScript types without checking them. Run
tsc --noEmitin CI, orbun checkon Bun 1.4.3+ if the team adopts it. - If you use a Node framework (Express, Fastify, Hono, Elysia), keep its server API
instead of rewriting to
Bun.serve; Bun implementsnode:http. bun run <script>executes package.json scripts; CLIs with a#!/usr/bin/env nodeshebang still run on Node unless invoked asbun --bun <cmd>.
5. Bundling and executables
bun build ./src/index.ts --outdir dist --target bun --minify --sourcemap=linked
bun build ./src/cli.ts --compile --outfile mycli # host platform
bun build ./src/cli.ts --compile --target=bun-linux-x64 --outfile mycli-linux
--target nodewhen the bundle will run on Node;--target browserfor frontend code.- Mark native or dynamic-require packages
--external <pkg>rather than bundling them.
6. Production image
FROM oven/bun:1 AS deps
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production
FROM oven/bun:1
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
USER bun
EXPOSE 3000
CMD ["bun", "src/index.ts"]
Pin the image to the exact Bun version used in CI (oven/bun:1.x.y) once the app is in
production; minor releases occasionally change behavior.
Gotchas
- Bun 1.4 (Aug 2026) rewrote the runtime in Rust. Treat upgrades across 1.3 to 1.4 as a runtime upgrade: run the full suite and a staging soak before rolling out.
process.memoryUsage()and heap snapshots differ from V8; Node-specific memory tooling does not apply. Use Bun’s documented profiling flags.- APM and tracing agents that patch Node internals may silently do nothing; confirm spans arrive before removing the Node deploy.
idleTimeoutdefaults to 10 seconds; long-polling and SSE endpoints die unless you raise it or callserver.timeout(req, 0)for that request.- Mixing package managers in one repo produces two dependency trees. Pick one.
Verification
bun install --frozen-lockfilesucceeds in CI from a clean checkout.bun test(or the retained runner) passes;tsc --noEmitpasses.- The production entrypoint runs under the exact image tag and answers
/healthz. - Send SIGTERM during a load test (
k6oroha): no 5xx for in-flight requests. - Compare p95 latency and memory between the Node and Bun builds on the same load; keep Node if Bun shows no measurable gain for the risk.
If a dependency fails only under Bun, reduce it to a single-file reproduction, check the Bun issue tracker, and either pin the working Bun version, replace the dependency, or keep that service on Node.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.