Bun Runtime skill for Codex
Adopt Bun without surprise Node fallbacks. Codex adopts Bun one layer at a time, package manager, tests, then runtime, and checks what still runs on Node along the way. It commits the text bun.lock, lists trustedDependencies for packages that need postinstall, keeps tsc in CI because Bun strips types without checking, and serves HTTP with Bun.serve routes plus a clean SIGTERM stop. Known traps like auto-loaded .env files are called out.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- bun-runtime Installs to ~/.agents/skills/bun-runtime/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install bun-runtime The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project bun-runtime Installs to .agents/skills/bun-runtime/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/bun-runtime.zipunzip -o bun-runtime.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) bun-runtime PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $bun-runtime, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Uses Bun as runtime, package manager, bundler and test runner - bun install with the text bun.lock and trustedDependencies, workspaces, Bun.serve with routes, Bun.file, bun:sqlite and Bun.sql, bun test with mocks and coverage, bun build --compile, and Docker images on oven/bun. Covers Node compatibility traps such as scripts that silently run on Node, unsupported native addons, automatic .env loading and missing type checks. Use when moving a Node project to Bun, starting a Bun service, or debugging behaviour that differs between Bun and Node.
Use it when
- Migrating a Node.js project to Bun as package manager, test runner or runtime
- Starting a new HTTP service or CLI on Bun
- Behaviour differs between Bun and Node and you need to find out why
- Producing a single-file executable with bun build --compile
Use something else when
- The project targets Deno or Deno Deploy: use deno-apps
- You need a framework-level API design on Hono regardless of runtime: use hono-edge-api
How to check the result
bun install --frozen-lockfile succeeds from a clean checkout, tsc --noEmit and bun test pass, and the service answers on its health route and exits cleanly on SIGTERM.
The mistake it prevents
Assuming bun run executes scripts on Bun, when tools with a node shebang still run on Node unless invoked with bun --bun.
Done well means: the team knows which parts run on Bun and which still run on Node, the
lockfile is the text bun.lock and committed, CI installs with --frozen-lockfile,
TypeScript is still type-checked by tsc, and every Node API the app depends on has been
exercised under Bun by a test, not assumed compatible.
Before changing anything
- Read
AGENTS.md,package.json(scripts,engines,workspaces,trustedDependencies),bunfig.tomland which lockfiles exist. - Check the Bun version (
bun --version) and pin it for the team (packageManagerfield,.bun-versionfor CI setup actions, or the Docker tag). - Decide the scope with the user. Using Bun only as a package manager and test runner is low risk; switching the production runtime needs a compatibility pass.
Adoption decision
| Goal | Do | Risk |
|---|---|---|
| Faster installs only | bun install, keep running on Node | Low; lifecycle scripts differ |
| Faster tests | bun test for unit tests | Medium; Jest/Vitest APIs not 1:1 |
| Run scripts and tooling | bun run, bunx | Low; scripts still use Node unless --bun |
| Production runtime | bun run src/index.ts in the container | Highest; test every Node API path |
| Single binary CLI | bun build --compile | Low for pure TS; native addons need care |
Package management
bun install # creates bun.lock (text, Bun 1.2+); commit it
bun install --frozen-lockfile # CI: fail if package.json and lockfile disagree
bun add zod; bun add -d @types/bun
bun pm trust <pkg> # allow a dependency's postinstall script
bunx prisma generate # like npx
- Bun does not run dependency lifecycle scripts (postinstall) unless the package is in
trustedDependenciesor on Bun’s default allow-list. Missing native builds (sharp,bcrypt,prismaengines) usually trace back to this. - Migrating from an old binary
bun.lockb: runbun install --save-text-lockfile(or any install on Bun 1.2+), commitbun.lock, deletebun.lockb. - Migrating from npm, yarn or pnpm:
bun installreads the existing lockfile once to preserve versions. Delete the old lockfile only afterbun.lockis committed and CI is green. - Workspaces: declare them in
package.jsonworkspaces;bun installlinks them. Check the[install] linkersetting inbunfig.toml(hoisted vs isolated) before debugging “module not found” in a monorepo. - Installs need network; in a Codex sandbox with network off, request approval.
Serving HTTP
const server = Bun.serve({
port: Number(process.env.PORT ?? 3000),
routes: {
"/healthz": new Response(null, { status: 204 }),
"/api/orders/:id": {
GET: async (req) => {
const order = await findOrder(req.params.id);
return order ? Response.json(order) : Response.json({ error: "not found" }, { status: 404 });
},
},
"/api/orders": {
POST: async (req) => Response.json(await createOrder(await req.json()), { status: 201 }),
},
},
fetch() {
return Response.json({ error: "not found" }, { status: 404 });
},
error(err) {
console.error(err);
return Response.json({ error: "internal" }, { status: 500 });
},
});
process.on("SIGTERM", async () => {
await server.stop(); // stop accepting, let in-flight requests finish
process.exit(0);
});
routes needs Bun 1.2.3+. Frameworks built on Web standard Request/Response (Hono,
Elysia) run natively; Express and Fastify run through Bun’s node:http layer and work
for most apps, but measure rather than assume a speedup.
Bun-native APIs
| Need | API | Note |
|---|---|---|
| Read a file | await Bun.file(path).text() / .json() / .arrayBuffer() | Lazy; exists() before reading |
| Write a file | await Bun.write(path, data) | Accepts strings, blobs, responses |
| SQLite | import { Database } from "bun:sqlite" | Synchronous; use prepared statements |
| Postgres | import { sql } from "bun" tagged template | Parameterises interpolations; check driver features you rely on |
| Password hashing | Bun.password.hash / verify | argon2id by default |
| Shell scripts | import { $ } from "bun" | Interpolations are escaped |
Using Bun-only APIs ties the code to Bun. Keep them behind a small module if the project might need to run on Node again.
Testing
import { describe, expect, it, mock, beforeEach } from "bun:test";
const sendEmail = mock(async () => ({ id: "msg_1" }));
mock.module("./email", () => ({ sendEmail }));
describe("createOrder", () => {
beforeEach(() => sendEmail.mockClear());
it("sends a confirmation", async () => {
const { createOrder } = await import("./orders");
await createOrder({ sku: "A1", quantity: 1 });
expect(sendEmail).toHaveBeenCalledTimes(1);
});
});
bun test # finds *.test.ts, *_test.ts, *.spec.ts
bun test --coverage
bun test --watch
mock.modulemust run before the module under test is imported; use a dynamicimport()after mocking, or apreloadscript inbunfig.toml([test] preload).- Jest globals and many matchers work, but timers, module mocking and snapshot formats differ. Port a few test files first and compare results with the old runner.
- DOM tests need
happy-domregistered in a preload; there is no built-in jsdom environment.
Build and deploy
bun build ./src/index.ts --outdir dist --target bun --minify
bun build ./cli.ts --compile --outfile mycli # standalone executable
FROM oven/bun:1 AS deps
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production
FROM oven/bun:1-slim
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
USER bun
CMD ["bun", "run", "src/index.ts"]
Pin the image to a specific Bun version for reproducible builds.
Node compatibility traps
bun run devexecutes the script’s own binary; if that binary has a#!/usr/bin/env nodeshebang (Vite, Next, tsc), it runs on Node. Usebun --bun run devto force Bun.- Bun loads
.env,.env.<NODE_ENV>and.env.localautomatically. A stale local.envcan override what you expect in tests; Node does not do this. - Bun strips TypeScript types without checking them. Keep
tsc --noEmitin CI. - Native addons using N-API generally work; addons tied to V8 or Node internals may not.
- Check Bun’s Node.js compatibility page for each
node:module the app uses beyond the common ones (vm,inspector,cluster,worker_threadsoptions,http2). process.versions.bunexists only under Bun; use it for the rare runtime branch.
Checklist
- Scope agreed (package manager, tests, runtime)
-
bun.lockcommitted; old lockfile removed after CI passes -
trustedDependencieslists packages that need install scripts -
tsc --noEmitandbun testpass; critical paths also run once under Node if both are supported - Docker image pinned; graceful stop on SIGTERM
Gotchas
Bun.servedefaultidleTimeoutis 10 s; long polling and SSE need a larger value orserver.timeout(req, 0)per request.bun:sqliteis synchronous and blocks the event loop on large queries.bunxmay use a cached package version; pin versions in CI (bunx [email protected]).
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Bun Runtime"
short_description: "Adopt Bun without surprise Node fallbacks"
default_prompt: "$bun-runtime Switch this Node project to bun install and bun test without breaking CI" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.