@code-yeongyu/senpi-codemode 2026.10.31-1 → 2026.11.2-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +1 -1
  3. package/package.json +4 -4
  4. package/src/extension/host-facts.ts +81 -3
  5. package/src/extension/skill-contribution.ts +6 -6
  6. package/src/kernels/js/cell-ownership.js +12 -3
  7. package/src/kernels/py/path_identity_windows.py +3 -1
  8. package/src/kernels/shared/subprocess-kernel.ts +4 -1
  9. package/src/kernels/shared/subprocess-process.ts +12 -0
  10. package/src/prompt/eval-prompt-template.ts +1 -1
  11. package/src/prompt/eval-prompt.ts +4 -2
  12. package/src/skill/bun/SKILL.md +53 -0
  13. package/src/skill/{bun-1-4 → bun}/references/breaking-changes.md +26 -4
  14. package/src/skill/{bun-1-4 → bun}/references/build.md +32 -6
  15. package/src/skill/bun/references/dev-tooling.md +67 -0
  16. package/src/skill/bun/references/http-networking.md +157 -0
  17. package/src/skill/bun/references/install.md +137 -0
  18. package/src/skill/{bun-1-4 → bun}/references/node-compat-platforms.md +16 -2
  19. package/src/skill/{bun-1-4 → bun}/references/performance.md +9 -3
  20. package/src/skill/{bun-1-4 → bun}/references/runtime-apis.md +38 -13
  21. package/src/skill/bun/references/security.md +74 -0
  22. package/src/skill/bun/references/session-recipes.md +71 -0
  23. package/src/skill/bun/references/test.md +132 -0
  24. package/src/tool/detached-cell-manager.ts +50 -3
  25. package/src/tool/eval-argument-validator.ts +89 -0
  26. package/src/tool/eval-language-inference.ts +116 -0
  27. package/src/tool/eval-request.ts +34 -13
  28. package/src/tool/eval-tool-options.ts +1 -1
  29. package/src/tool/eval-tool.ts +17 -0
  30. package/src/tool/types.ts +2 -0
  31. package/src/skill/bun-1-4/SKILL.md +0 -45
  32. package/src/skill/bun-1-4/references/dev-tooling.md +0 -34
  33. package/src/skill/bun-1-4/references/http-networking.md +0 -113
  34. package/src/skill/bun-1-4/references/install.md +0 -93
  35. package/src/skill/bun-1-4/references/security.md +0 -52
  36. package/src/skill/bun-1-4/references/test.md +0 -103
@@ -0,0 +1,157 @@
1
+ # Bun 1.4.x HTTP & Networking
2
+
3
+ ## Serve files & folders (replaces express.static, serve-static, sirv) [v1.4.0]
4
+
5
+ Docs: <https://bun.com/docs/runtime/http/routing> · Blog: [#serve-files-folders](https://bun.com/blog/bun-v1.4#serve-files-folders)
6
+
7
+ ```ts
8
+ Bun.serve({
9
+ routes: {
10
+ "/static/*": { dir: "./public" },
11
+ },
12
+ });
13
+ ```
14
+
15
+ - Files stream with `sendfile`; `Content-Type`, `ETag`, `Last-Modified`, `304`, and `Range` handled automatically; `index.html` served for directories.
16
+ - Path traversal is blocked: paths normalized, and on Linux files open with `openat2` + `O_RESOLVE_BENEATH` so symlinks can't escape the directory.
17
+
18
+ ## Range and conditional requests [v1.3.13, improved v1.4.0]
19
+
20
+ Blog: [#range-and-conditional-requests](https://bun.com/blog/bun-v1.4#range-and-conditional-requests)
21
+
22
+ ```ts
23
+ Bun.serve({
24
+ routes: { "/video.mp4": new Response(Bun.file("./video.mp4")) },
25
+ });
26
+ // curl -H 'Range: bytes=0-1023' → 206 Partial Content
27
+ // curl -H 'If-None-Match: "..."' → 304 Not Modified
28
+ ```
29
+
30
+ Static routes and `Bun.file()` bodies handle `Range` (206), `If-None-Match`/`If-Modified-Since` (304), `If-Match`/`If-Unmodified-Since` (412), and `If-Range` (206 only if unchanged, else 200 with the full body) [v1.4.3]. Video seeking and resumable downloads work out of the box.
31
+
32
+ ## HTTP/2 [v1.4.1] and HTTP/3 (EXPERIMENTAL) in Bun.serve()
33
+
34
+ Docs: <https://bun.com/docs/runtime/http/server> · Blog: [1.4.1 #bun-serve-supports-http-2](https://bun.com/blog/bun-v1.4.1), [#http-3-in-bun-serve-experimental](https://bun.com/blog/bun-v1.4#http-3-in-bun-serve-experimental)
35
+
36
+ ```ts
37
+ Bun.serve({
38
+ port: 443,
39
+ tls: { /* cert, key */ },
40
+ http2: true, // same port as HTTP/1.1; ALPN over TLS, h2c preface on cleartext; http1: false to refuse HTTP/1.x
41
+ http3: true, // also listen on UDP/443
42
+ fetch(req) { return new Response("hi"); }, // same routes/fetch for every protocol
43
+ });
44
+ ```
45
+
46
+ - HTTP/2: 6.8x the requests/s of `node:http2` on hello-world; WebSockets and response trailers are not yet supported over H2.
47
+ - HTTP/3: `Alt-Svc` advertises H3 so browsers upgrade themselves; 2.7x faster than HTTPS/1.1 on static routes. **Do not ship `http3: true` to production yet**: 0-RTT resumption disabled, `server.upgrade()` returns `false` over H3, `unix:` sockets skip the H3 listener.
48
+
49
+ ## HTTP/2 & HTTP/3 in fetch() — EXPERIMENTAL [v1.3.14, improved v1.4.0]
50
+
51
+ Blog: [#http-2-http-3-in-fetch-experimental](https://bun.com/blog/bun-v1.4#http-2-http-3-in-fetch-experimental)
52
+
53
+ ```ts
54
+ const [a, b] = await Promise.all([
55
+ fetch(url1, { protocol: "http2" }), // concurrent same-origin requests share one connection
56
+ fetch(url2, { protocol: "http2" }),
57
+ ]);
58
+ const res = await fetch(url3, { protocol: "http3" });
59
+ ```
60
+
61
+ - Redirects, decompression, streaming behave as over HTTP/1.1.
62
+ - Global opt-in: `BUN_FEATURE_FLAG_EXPERIMENTAL_HTTP2_CLIENT=1` or `--experimental-http3-fetch` (Bun remembers per-origin H3 support).
63
+
64
+ ## fetch() request compression [v1.4.0]
65
+
66
+ Blog: [#fetch-request-compression](https://bun.com/blog/bun-v1.4#fetch-request-compression)
67
+
68
+ ```ts
69
+ await fetch(url, {
70
+ method: "POST",
71
+ body: largeJsonString,
72
+ compress: "gzip", // or true, "deflate", "br", "zstd", { encoding, level }
73
+ });
74
+ ```
75
+
76
+ Buffered bodies (string, ArrayBuffer, TypedArray, Blob) are compressed and `Content-Encoding`/`Content-Length` set automatically; streaming bodies pass through unchanged.
77
+
78
+ ## fetch() proxies [v1.3.4, env support v1.4.3, proxy.tls v1.4.4]
79
+
80
+ Blog: [#fetch-proxy-headers](https://bun.com/blog/bun-v1.4#fetch-proxy-headers), [1.4.3 #better-proxy-environment-support](https://bun.com/blog/bun-v1.4.3)
81
+
82
+ ```ts
83
+ await fetch(url, {
84
+ proxy: {
85
+ url: "https://proxy.example.com:8443",
86
+ headers: { "Proxy-Authorization": "Bearer token" },
87
+ tls: { ca: Bun.file("./proxy-ca.pem") }, // proxy-only TLS options; `tls` stays for the origin [v1.4.4]
88
+ },
89
+ });
90
+ await fetch(url, { proxy: false }); // ignore HTTP(S)_PROXY / ALL_PROXY for one request
91
+ ```
92
+
93
+ - `NO_PROXY` accepts wildcards, CIDR blocks, bare IPv6 and `host:port`; `ALL_PROXY` is used when `HTTP_PROXY`/`HTTPS_PROXY` are unset; each redirect hop picks its proxy by its own URL.
94
+ - A proxy that refuses `CONNECT` rejects with `ERR_PROXY_TUNNEL` carrying the proxy's `status` and `headers` (before 1.4.3 the proxy's reply resolved as if from the origin).
95
+
96
+ ## Bun.FetchSession — per-client TLS, proxy and connection pool [v1.4.3]
97
+
98
+ ```ts
99
+ using session = new Bun.FetchSession({
100
+ tls: { ca: await Bun.file("corp-ca.pem").text() },
101
+ proxy: { url: "http://proxy.internal:8080" },
102
+ keepAlive: { idleTimeout: 30, maxIdleSockets: 8 },
103
+ });
104
+ const client = new SomeSdk({ fetch: session.fetch }); // bound; drop-in for any `fetch`-accepting client
105
+ await fetch("https://example.com", { session }); // same thing
106
+ ```
107
+
108
+ Sessions never share connections with each other or with plain `fetch()`; per-request options override the session's; `tls.checkServerIdentity` runs once per connection. Replaces undici `Agent` / `dispatcher` plumbing.
109
+
110
+ ## Bun.write(path, response) streams to disk [v1.4.1]
111
+
112
+ ```ts
113
+ await Bun.write("./big.tar.gz", await fetch(url)); // => bytes written; 128 MiB download adds 13 MB RSS (was 161 MB)
114
+ await Bun.write("./out.txt", readableStream); // was "[object ReadableStream]"
115
+ ```
116
+
117
+ `fetch()` and S3 downloads pause when 256 KiB of body is unread; a `Response` discarded unread closes its connection. `fetch()` over `{ unix }` sockets now reuses connections, and `localhost` / `*.localhost` resolve to loopback without the system resolver (works in IPv4-only containers) [v1.4.1]. `BUN_CONFIG_VERBOSE_FETCH=curl` prints each request as a `curl` line (see [session-recipes](session-recipes.md)).
118
+
119
+ ## TLS session resumption + connection reuse [v1.3.10-v1.4.0]
120
+
121
+ Blog: [#tls-session-resumption](https://bun.com/blog/bun-v1.4#tls-session-resumption), [#connection-reuse](https://bun.com/blog/bun-v1.4#connection-reuse)
122
+
123
+ - Second cold connection to an origin resumes at 1 RTT (32-entry per-origin LRU of BoringSSL client sessions).
124
+ - `fetch()` reuses connections through HTTPS proxies and for requests with custom TLS options (client cert, custom CA). No code change needed.
125
+
126
+ ## HTML routes: sourcemaps off in production [v1.4.0]
127
+
128
+ Blog: [#html-routes-sourcemaps-disabled-in-production](https://bun.com/blog/bun-v1.4#html-routes-sourcemaps-disabled-in-production)
129
+
130
+ Production `Bun.serve` no longer serves sourcemaps for HTML routes (dev mode still does). Override in `bunfig.toml`:
131
+
132
+ ```toml
133
+ [serve.static]
134
+ sourcemap = "linked"
135
+ ```
136
+
137
+ ## WebSocket: backpressure and close semantics [v1.4.1, v1.4.4]
138
+
139
+ ```ts
140
+ socket.pause(); socket.isPaused; socket.resume(); // client WebSocket [v1.4.1] and Bun.serve `ws` [v1.4.4]: stop reading, peer sees TCP backpressure
141
+ socket.bufferedAmount; // bytes still queued to send (was always 0)
142
+ ```
143
+
144
+ - The client `close` event now fires after the closing handshake with the server's code (`1005` when its Close frame had none, `1006` after 30 s / `BUN_CONFIG_WS_CLOSE_TIMEOUT`); a script ending with `ws.close()` takes one extra round trip to exit; `ws.terminate()` closes at once [v1.4.4].
145
+ - A failed connection fires `error` (`event.error.code`: `ECONNREFUSED`, `ERR_TLS_CERT_ALTNAME_INVALID`, `WS_ERR_INVALID_UTF8` …) then `close` with `1006` [v1.4.4].
146
+ - `import "ws"` is a port of ws 8.22.0 on Bun's native sockets [v1.4.4]: `createWebSocketStream`, `agent`/SOCKS, `handshakeTimeout`, `maxPayload` all work; an `'error'` event without a listener is an uncaught exception, as in Node — add one to every socket. `ws` servers have no idle timeout or ping by default (Bun.serve's 120 s / 104 s no longer apply).
147
+ - `Bun.serve` `websocket.error(error, ws)` now receives the socket; every handler including `message` is optional [v1.4.4].
148
+
149
+ ## Server lifecycle worth knowing
150
+
151
+ - `idleTimeout` defaults to **10 s** of no bytes in either direction — a handler that computes for 11 s before writing gets its connection reset. Raise it (max 255, `0` disables) or per request: `server.timeout(req, 60)`.
152
+ - `server.reload({ fetch, routes, websocket, error })` swaps handlers without restarting; `server.requestIP(req)`; `server.pendingRequests` / `pendingWebSockets` / `subscriberCount(topic)` for health checks.
153
+ - `server.stop()` closes idle keep-alive connections immediately, waits for in-flight responses, and resolves when the last connection closes; `server.stop(true)` force-closes (WebSockets get `1001 "Server closed"` [v1.4.4]).
154
+ - `server.publish()` / `ws.publish()` return `0` (dropped/no subscribers) or `-1` (backpressure) instead of always the byte count.
155
+ - Per-method route objects (`{ GET }`) answer `HEAD` with the GET handler automatically.
156
+ - `Bun.serve({ unix: "/tmp/app.sock" })` and `fetch(url, { unix })` for local IPC; `reusePort: true` runs a cluster of processes on one port.
157
+ - **Test servers bind `hostname: "127.0.0.1"`.** `Bun.serve({ port: 0 })` binds the wildcard address, so another process's loopback listener on the same port wins and requests cross-talk (1 in ~20 on a busy host, 2026-10-08). A 10 s `idleTimeout` also cut an SSE proxy's upstream streams mid-response (2026-10-02).
@@ -0,0 +1,137 @@
1
+ # bun install & package management in 1.4.x
2
+
3
+ ## Session rules first — each one cost a past session hours
4
+
5
+ - **Unattended installs block on prompts.** A repo's `prepare` script can ask a question (desktop's `vp config` git-hook prompt sat at 0% CPU for 14 min); run installs with `CI=1`, stdin closed, visible output in a background bash session — never `bun install | tail` (2026-10-15/19/20). If a process shows 0 CPU and no network while the package set stopped changing, it is prompting or wedged: inspect the child, do not start tests.
6
+ - **`--ignore-scripts` leaves `node_modules/bun` as a placeholder.** When a repo pins `bun` as a devDependency, the npm `bun` package's bin is a stub until its postinstall runs; with `node_modules/.bin` first on PATH every `bun …` script then fails ("Bun's postinstall script was not run", ENOEXEC). Run `(cd node_modules/bun && node install.js)` after an `--ignore-scripts` install, or keep the system `bun` ahead on PATH (2026-10-06, 10-26, 10-31). Never `bun add bun` anywhere: it installs that placeholder over the real runtime; upgrade with `bun upgrade`.
7
+ - **Global installs stay task-local.** A global npm/bun install can replace `~/.bun/bin/bun` with a broken shim and a stray `bun` in `~/.bun/install/global/node_modules/.bin` shadows the real one on PATH. Use `BUN_INSTALL=$RUN_TMP/bun-global` for globals a task needs (2026-10-10, 10-22).
8
+ - **`bun add` with no package.json in cwd edits the nearest ancestor** — it once rewrote `~/package.json` + `~/bun.lock` (2026-10-27). Check `pwd` first.
9
+ - **Evidence runs use the repo's pinned Bun**, not the machine's newest: the pin lives in CI (`bun-version:`) and often `package.json` `devDependencies.bun`; 1.4.2 vs 1.4.3 differed in test behaviour, and two jobs swapping a box-wide pin corrupted each other's evidence (2026-10-16/18).
10
+ - **`--frozen-lockfile` refuses only missing packages**; it does not prove `node_modules` matches, and a post-rebase `bun install` can rewrite `bun.lock` with incidental range churn — diff the lockfile before committing (2026-09-21, 10-05, 10-26). `~/.bun/install/cache` is shared by every project: never delete it for disk space; `bun pm cache prune --max-age 7` is the tool.
11
+ - **Symlinked `node_modules` from another checkout is stale and mutable**: package scripts rewrite files in place, so every concurrent run gets its own install (2026-09-30). Concurrent installs in worktrees that share one `.git` contend on its lock — stagger them (2026-10-19).
12
+
13
+ ## Global virtual store: up to 7x faster installs [v1.3.14]
14
+
15
+ Blog: [#global-virtual-store-up-to-7x-faster-installs](https://bun.com/blog/bun-v1.4#global-virtual-store-up-to-7x-faster-installs)
16
+
17
+ ```toml
18
+ # bunfig.toml
19
+ [install]
20
+ linker = "isolated"
21
+ ```
22
+
23
+ With the isolated linker, packages extract once into Bun's global cache and are symlinked into `node_modules/.bun/` (one `symlink()` per package instead of one `clonefileat()`). Warm-cache CI installs of 1,400 packages: 7x faster. Opt-in for existing projects; **new monorepos default to isolated** (see below).
24
+
25
+ ## Offline, cached and CI installs [v1.4.1, v1.4.4]
26
+
27
+ Blog: [1.4.1 #bun-install-offline](https://bun.com/blog/bun-v1.4.1), [1.4.4 #bun-pm-fetch](https://bun.com/blog/bun-v1.4.4)
28
+
29
+ ```bash
30
+ bun install --prefer-offline # use cached manifests regardless of age; fetch only what is missing — fast fresh worktrees
31
+ bun install --offline # no network at all; a missing package is a named error
32
+ bun pm fetch # download every dependency into the cache; no node_modules, no scripts, lockfile untouched
33
+ bun pm cache pack .ci/bun-cache.pack # after an install: one file with the cache entries the lockfile needs
34
+ bun pm cache unpack .ci/bun-cache.pack && bun install --frozen-lockfile # on another runner
35
+ bun pm cache prune --max-age 7 # drop old cache entries (--dry-run lists)
36
+ BUN_CONFIG_HTTP_RETRY_BACKOFF=250 bun install # registry retries 250 ms → 4 s (default 50 ms → 800 ms, ±20% jitter, Retry-After honoured) [v1.4.4]
37
+ ```
38
+
39
+ Docker: keep downloads in a layer that only changes with the lockfile, build with devDeps, ship without:
40
+
41
+ ```dockerfile
42
+ COPY package.json bun.lock ./
43
+ RUN bun pm fetch
44
+ COPY . .
45
+ RUN bun install --offline --frozen-lockfile
46
+ RUN bun run build && bun prune --production
47
+ ```
48
+
49
+ `bunfig.toml`: `[install] offline = true` / `prefer = "offline"`. Linux installs skip the native build for the other libc (`--libc musl` for an Alpine image built elsewhere, `--libc '*'` for both) [v1.4.4].
50
+
51
+ ## Diagnose and fix the tree [v1.4.0, v1.4.4]
52
+
53
+ Blog anchors: [#bun-pm-diff](https://bun.com/blog/bun-v1.4#bun-pm-diff), [#bun-audit-fix](https://bun.com/blog/bun-v1.4#bun-audit-fix), [#bun-dedupe](https://bun.com/blog/bun-v1.4#bun-dedupe), [#bun-prune](https://bun.com/blog/bun-v1.4#bun-prune), [#bun-pm-licenses](https://bun.com/blog/bun-v1.4#bun-pm-licenses)
54
+
55
+ ```bash
56
+ bun why lodash # which dependents pull a package in (deduped tree); bun pm ls lodash lists installed copies
57
+ bun pm diff react # lockfile version → latest; un-minifies before diffing;
58
+ bun pm diff react@18.2.0 19.0.0 # summary flags new install scripts + new child_process/fs/net/vm imports
59
+ bun outdated # what bun update would change; bun update --depth 0 touches only package.json deps [v1.4.4]
60
+ bun audit fix # upgrade vulnerable packages to safe versions; --latest allows majors; --dry-run
61
+ bun pm scan # security-scanner pass over the whole lockfile
62
+ bun dedupe # collapse duplicate versions in bun.lock; --check fails CI on dupes
63
+ bun prune --production # delete node_modules entries not in bun.lock; --check only reports (exit 1 if anything) [v1.4.4]
64
+ bun pm licenses --prod --json # dependency license inventory
65
+ bun patch pkg && bun patch --commit pkg # edit a dependency in place; never hand-edit the .patch (2026-09-11)
66
+ ```
67
+
68
+ `bun install` warns about deprecated packages and prefers non-deprecated versions when resolving a range [v1.4.4].
69
+
70
+ ## bun update: transitive + patterns [v1.4.0]
71
+
72
+ Blog: [#bun-update-updates-transitive-dependencies](https://bun.com/blog/bun-v1.4#bun-update-updates-transitive-dependencies)
73
+
74
+ ```bash
75
+ bun update # now updates deps-of-deps too
76
+ bun update zod # updates zod everywhere it appears
77
+ bun update '@types/*' --latest
78
+ ```
79
+
80
+ `bun update <name>` errors (exit 1) if nothing depends on it (no longer silently adds). `bun add --dev x` moves an existing dependency between groups instead of leaving it [v1.4.4].
81
+
82
+ ## Monorepo: --filter and --catalog [v1.4.0]
83
+
84
+ Blog: [#bun-add-filter](https://bun.com/blog/bun-v1.4#bun-add-filter), [#bun-add-catalog](https://bun.com/blog/bun-v1.4#bun-add-catalog)
85
+
86
+ ```bash
87
+ bun add zod --filter api # add to one workspace from the repo root
88
+ bun run --filter 'web...' build # web + its dependencies; '...web' = its dependents
89
+ bun add react --catalog # add to root catalog, write "catalog:" in the workspace
90
+ ```
91
+
92
+ Plain `bun add x` in a workspace whose default catalog lists `x` writes `catalog:` automatically. `--filter '{packages}'` selects every workspace under a directory; `bun publish --recursive --filter "@acme/*"` publishes a monorepo in dependency order, skipping private and already-published versions [v1.4.4].
93
+
94
+ - `"workspaces": { "packages": [...], "selfContained": ["apps/desktop"] }` gives a workspace its own full `node_modules` (Electron and other layout-sensitive tools) [v1.4.1].
95
+ - `"packageExtensions": { "legacy-plugin@2": { "dependencies": { "lodash": "^4" } } }` adds a dependency a package forgot to declare — the isolated linker needs it [v1.4.4].
96
+ - `"dep": "link:./vendor/dep"` symlinks a directory without `bun link` [v1.4.4]; `file:` folder deps are installed as hardlinks in 1.4.4 (per-file symlinks in 1.4.3), which changes what "no links in node_modules" checks see (senpi#4223).
97
+ - With the isolated linker, a root-level non-workspace directory (`scripts/`) cannot import `@scope/pkg`; make it a workspace package (2026-10-02).
98
+
99
+ ## Nested overrides [v1.4.0]
100
+
101
+ Blog: [#nested-overrides](https://bun.com/blog/bun-v1.4#nested-overrides)
102
+
103
+ ```json
104
+ {
105
+ "overrides": {
106
+ "express": { "qs": "6.13.0" },
107
+ "lodash@<4.17.21": "4.17.21"
108
+ }
109
+ }
110
+ ```
111
+
112
+ npm nested form, yarn `a/b`, pnpm `a>b` all work; overrides can be version-scoped. Lockfiles using these become `lockfileVersion: 3` (older Bun can't read them). Overrides are NOT applied to an `npm:` alias spec (2026-10-02). Peer dependencies resolve like pnpm since 1.4.4: a peer nobody provides gets a version inside its range instead of whatever another package installed.
113
+
114
+ ## Supply-chain hardening
115
+
116
+ - **Lockfile integrity** [v1.3.10]: `bun.lock` records SHA-512 for GitHub and tarball deps, like npm packages.
117
+ - **`trustedDependencies` auto-trust is npm-registry-only** [v1.3.5]: a `git:`/`github:`/`file:` package named `esbuild` gets NO trust from the default list — list it yourself. Names match exactly (not by hash).
118
+ - **`nativeDependencies`** [v1.3.2]: for packages shipping prebuilt binaries as per-platform optionalDependencies, Bun links the right binary directly instead of running postinstall. **`ignoreScripts`** skips a package's lifecycle scripts even if trusted.
119
+
120
+ ```json
121
+ { "nativeDependencies": ["esbuild"], "ignoreScripts": ["sharp"] }
122
+ ```
123
+
124
+ ## Flags, env and bunx [v1.4.4]
125
+
126
+ - `bun install` translates npm/Yarn/pnpm flags (`--immutable`, `--save-dev`, `--node-linker=isolated` …) and **rejects unknown flags** — `--frozen-lockfil` used to run a plain install silently in CI.
127
+ - `bun install --env-file ci.env`, `bunx --env-file ci.env prisma migrate deploy`, `bunx --cwd packages/api <tool>`, `bunx -p pkg cmd` (pkg's bins first on PATH), `bunx --trust` (run the tool's lifecycle scripts).
128
+ - `[install] minimumReleaseAge = "3d"` with `minimumReleaseAgeExcludes = ["typescript@5.9.3"]` delays fresh releases except a named one.
129
+ - `NPM_CONFIG_USERCONFIG` from `actions/setup-node` is read, so private registries work in CI without copying `.npmrc`.
130
+
131
+ ## Defaults changed in 1.4.x (see also breaking-changes.md)
132
+
133
+ - New monorepos default to `linker: "isolated"` (`configVersion: 1` in bun.lock). Existing lockfiles keep hoisted. Pin `linker = "hoisted"` to opt out.
134
+ - `bun.lock` is `lockfileVersion: 2`: out-of-registry tarballs need integrity hashes; git entries validated against path traversal. Run `bun install` to migrate. 1.4.4 adds `hasInstallScript` and `libc` keys that older Bun drops on its next save.
135
+ - A project's `bunfig.toml` now overrides `.npmrc` for the same key.
136
+ - `bun init` writes `typescript ^7`; non-TTY `bun init` behaves as `-y`.
137
+ - Yarn Berry lockfiles migrate all-or-nothing [v1.4.4]; `bun pm migrate` on a 20k-entry Yarn v1 lock takes 0.27 s.
@@ -1,12 +1,25 @@
1
- # Node.js Compatibility & Platforms (Bun 1.4)
1
+ # Node.js Compatibility & Platforms (Bun 1.4.x)
2
2
 
3
3
  Blog: [#node-js-compatibility](https://bun.com/blog/bun-v1.4#node-js-compatibility), [#platforms](https://bun.com/blog/bun-v1.4#platforms) · Live tracker: <https://bun.com/node-test-suite>
4
4
 
5
+ ## Packages Bun replaces with its own implementation (even when installed)
6
+
7
+ `undici` (its `fetch`/`Request`/`Response`/`WebSocket` are Bun's; `request()` calls Bun's fetch), `node-fetch`, `isomorphic-fetch`, `@vercel/fetch`, `ws` (a port of ws 8.22.0 on native sockets since 1.4.4), `utf-8-validate`, `abort-controller`, and Next.js's compiled copies of them. A `undici.install()` in a bundle once stalled every SSE stream (senpi#1890) — do not shim what Bun already owns.
8
+
9
+ ## Semantics that differ from Node and bit our tests
10
+
11
+ - `process.on("uncaughtExceptionMonitor")` is NOT emitted for an unhandled rejection (Node emits it with origin `unhandledRejection`); a monitor-only crash hook misses rejections (2026-10-03). In `bun test`, an unhandled rejection fails the running test before any `process.on("unhandledRejection")` handler sees it (2026-09-30).
12
+ - `subprocess.signalCode` is set when the signal is SENT, not when the child exits; await `proc.exited` with a deadline, then escalate (2026-10-02).
13
+ - `fs.rm` / `rmSync` parse but ignore `maxRetries`/`retryDelay` (bun#41480) — Windows EBUSY teardown needs an explicit retry loop (2026-09-27).
14
+ - `bun -eCODE` keeps the glued code in `process.execArgv`; re-spawning a host with the caller's execArgv replays it — strip `-e*`/`-p*` first (2026-10-03).
15
+ - macOS: a recursive `fs.watch(dir)` fired 1 event for 20 cross-process appends; watch the file itself once it exists (2026-10-30).
16
+
5
17
  Bun 1.4 reports **Node.js 26** (`process.versions.modules` = 147). 3,743 Node test-suite files pass (+1,517 since 1.3). `node:http`, `node:fs`, `node:cluster`, `node:timers`, `node:zlib`, `node:vm`, `node:stream` pass 97% of Node's own tests; `node:quic` 99%; `node:events`, `node:trace_events`, `node:sqlite` 100%.
6
18
 
7
19
  ## Big frameworks/tools that now run on Bun
8
20
 
9
21
  - **Playwright** [v1.4.0]: `connectOverCDP()`, `playwright test` with config, `--ui`, Chromium on Windows.
22
+ - **gRPC over `node:http2`** [v1.4.1]: interop fixes against `@grpc/grpc-js`, grpc-go and nghttp2; `better-sqlite3@13` loads [v1.4.1]; Slack Bolt socket mode reconnect loop fixed (undici `ping()` + diagnostics channels) [v1.4.4].
10
23
  - **Next.js 16** [v1.3.2]: `bun --bun next build` works on 16.3 with Turbopack + React Compiler.
11
24
  - **vitest** [v1.4.0]: runs under Bun including `--coverage`, threads and forks pools.
12
25
  - **OpenTelemetry** [v1.4.0]: http/fs instrumentation export spans; shimmer + require-in-the-middle patch bundled code.
@@ -28,7 +41,8 @@ Nuxt (`nuxt dev` HMR + DevTools), testcontainers/dockerode (`container.exec()`),
28
41
 
29
42
  | Platform | Status |
30
43
  |---|---|
31
- | FreeBSD x86_64/aarch64 [v1.3.14] | Official native builds; full runtime on stock FreeBSD 14.3+ (native port, not Linux compat layer) |
44
+ | FreeBSD x86_64/aarch64 [v1.3.14] | Official native builds; built against FreeBSD 14.4 since 1.4.4 (14.3 is end of life) |
45
+ | Docker `oven/bun:alpine` | Alpine 3.24 since 1.4.3; `bun install --libc musl` when building the image elsewhere |
32
46
  | Windows ARM64 [v1.3.7] | Native builds (Surface, Snapdragon X, Ampere) |
33
47
  | Android aarch64/x64 [v1.4.0] | Experimental builds with every release |
34
48
  | Linux glibc 2.17 [v1.3.13] | Minimum dropped from 2.26 — RHEL/CentOS 7, Amazon Linux 1 work; kernel minimum 3.10 (`memfd_create` fallback) |
@@ -1,8 +1,14 @@
1
- # Bun 1.4 Performance
1
+ # Bun 1.4.x Performance
2
2
 
3
- Blog: [#production](https://bun.com/blog/bun-v1.4#production), [#faster](https://bun.com/blog/bun-v1.4#faster)
3
+ Blog: [#production](https://bun.com/blog/bun-v1.4#production), [#faster](https://bun.com/blog/bun-v1.4#faster), then the 1.4.1/1.4.3/1.4.4 posts
4
4
 
5
- All of this is free — no code changes. Use these numbers when justifying a Bun 1.4 upgrade.
5
+ All of this is free — no code changes. Use these numbers when justifying a Bun upgrade; measure with `proc.resourceUsage()` / `process.memoryUsage().rss` (see [session-recipes](session-recipes.md)), and remember that `--cpu-prof-md` credits idle gaps to the last sampled frame — count samples before filing a hotspot (2026-10-17).
6
+
7
+ ## Since 1.4.0 (point releases)
8
+
9
+ - 1.4.1: JIT code is dropped after an idle period — Next.js SSR idle RSS 222 MB → 142 MB; `AsyncLocalStorage.run` 2x faster with no per-`await` allocation; `Buffer` read/write up to 9x; first HTTPS request 3x faster (DER-embedded roots); `Bun.inspect` on 16k-key objects 43x; compiled executables start 20% faster and bytecode is 3x smaller.
10
+ - 1.4.3: idle CPU down 67-89% (the GC timer no longer wakes every second); `Promise.allSettled` over 100k rejections 24 s → 148 ms; `JSON.parse` 1.2-1.5x on registry-shaped data; `node:http` large responses up to 2.7x; `Bun.gc(true)` returns memory to the OS immediately; `crypto.randomInt`, small zstd/zlib and `Bun.color` regressions from 1.4.0 recovered.
11
+ - 1.4.4: `bun check` 1.4-7.8x faster (`-b` up to 15x); hoisted installs from a warm cache 3.2x on Linux; `Bun.SQL` 21-48% less client CPU; `toLocaleString()` 54x; `Headers` iteration up to 2.8x; in-memory `response.body` streams up to 2.6x; `Bun.Glob.match` 1.6-5x; `Bun.JSON5.parse` 1.6x.
6
12
 
7
13
  ## Production footprint
8
14
 
@@ -1,6 +1,6 @@
1
- # Bun 1.4 Runtime Builtins
1
+ # Bun 1.4.x Runtime Builtins
2
2
 
3
- Every API here ships inside the Bun binary: no install step, no native build, no lockfile entry. Version tags mark the minimum Bun version.
3
+ Every API here ships inside the Bun binary: no install step, no native build, no lockfile entry. Version tags mark the minimum Bun version. Shell, spawn, secrets, files and the measurement helpers live in [session-recipes](session-recipes.md).
4
4
 
5
5
  ## Bun.Image — image processing (replaces sharp) [v1.3.14]
6
6
 
@@ -35,6 +35,8 @@ await Bun.write("page.png", await view.screenshot());
35
35
 
36
36
  - macOS: system WebKit, nothing to install. macOS/Linux/Windows: can drive installed Chrome/Chromium/Edge.
37
37
  - In the eval js kernel, Chrome-backed views (the default off macOS, or `backend: "chrome"`) are served by the process main thread; the API is the same, a constructor error surfaces on the first awaited call, and `Bun.WebView.closeAll()` closes only this kernel's views. A kernel reset closes every view the kernel left open.
38
+ - **One JS thread per process.** The WebKit backend is a process-wide client bound to the first thread that creates a view; a second thread (a Worker, a kernel worker) using it corrupts the JSC heap (omo SIGSEGV, 2026-10-29). The first Chrome-backed navigation (CDP target attach) is what blows a cell deadline, not Chrome startup — size test and cell timeouts so the inner one fires first (2026-09-28).
39
+ - Prefer `view.screenshot()` over `chrome --headless --screenshot`, which hung as an orphan on macOS (2026-10-19). Backend choice changes `Secure` cookie behaviour on loopback (2026-10-07). 1.4.1 fixed the hang after 64 WebKit views in one process.
38
40
  - Clicks/scrolls are trusted user input (`event.isTrusted === true`).
39
41
  - Extends `EventTarget`; screenshots are `Blob`s; `.cdp(method, params?)` escape hatch for raw Chrome DevTools Protocol.
40
42
 
@@ -45,16 +47,12 @@ Docs: <https://bun.com/docs/runtime/markdown> · Blog: [#bun-markdown](https://b
45
47
  ```ts
46
48
  const html = Bun.markdown.html("# Hello **world**"); // HTML string
47
49
  const el = Bun.markdown.react(readme); // React elements; swap components per tag
48
- const ansi = Bun.markdown.render("# Hi\n\n**bold**", { // callback per element (terminal output etc.)
49
- heading: (children) => `\x1b[1;4m${children}\x1b[0m\n`,
50
- paragraph: (children) => children + "\n",
51
- strong: (children) => `\x1b[1m${children}\x1b[22m`,
52
- });
50
+ const ansi = Bun.markdown.ansi("# Hi\n\n**bold**"); // terminal-ready ANSI (what `bun ./README.md` prints)
51
+ Bun.markdown.render(src, { heading: (children, { level }) => … }); // callback per element for any other target
53
52
  ```
54
53
 
55
- - GFM tables, strikethrough, task lists, autolinks. `.md` is a bundler loader. Linear time on adversarial input.
54
+ - GFM tables, strikethrough, task lists, autolinks. `.md` is a bundler loader. Linear time on adversarial input (1.4.3 fixed quadratic `render()` on deep nesting).
56
55
  - **HTML output is NOT sanitized** — raw HTML, event handlers, `javascript:` hrefs pass through. Sanitize before serving user content.
57
- - Bonus: `bun ./README.md` renders Markdown to the terminal (replaces glow).
58
56
 
59
57
  ## Bun.cron() — scheduled jobs (replaces node-cron) [v1.3.11, improved v1.4.0]
60
58
 
@@ -89,7 +87,7 @@ const proc = Bun.spawn(["bash"], {
89
87
  proc.terminal.write("echo Hello from PTY!\n");
90
88
  ```
91
89
 
92
- - Drive bash/vim/htop with colored output; resize; works on Linux, macOS, Windows.
90
+ - Drive bash/vim/htop with colored output; resize; works on Linux, macOS, Windows. Replaces node-pty and the `script -q /dev/null` wrapper our PTY test harnesses used.
93
91
  - `write()` returns full input length (whole input buffered); `drain` fires on POSIX.
94
92
 
95
93
  ## bun run --parallel — concurrent scripts (replaces concurrently, npm-run-all) [v1.3.9, improved v1.4.0]
@@ -132,8 +130,10 @@ Blog: [#also-built-in](https://bun.com/blog/bun-v1.4#also-built-in)
132
130
  | `Bun.JSONC.parse()` | jsonc-parser | comments + trailing commas (the tsconfig parser); throws `SyntaxError` on invalid input |
133
131
  | `Bun.XML.parse()/stringify()` | fast-xml-parser, xml2js | SIMD; `.xml` files importable (v1.4: import returns parsed doc, not path) |
134
132
  | `Bun.TOML.parse()/stringify()` | @iarna/toml | TOML v1.1.0, 708/708 toml-test; v1.4 is strict — unquoted strings throw `SyntaxError` |
135
- | `Bun.YAML.parse()` | — | YAML 1.2: `yes/no/on/off` are strings, not booleans |
136
- | `Bun.Archive` | tar | create/extract tarballs off the main thread — docs <https://bun.com/docs/runtime/archive> |
133
+ | `Bun.YAML.parse()` | js-yaml | YAML 1.2: `yes/no/on/off` are strings, not booleans |
134
+ | `Bun.Archive` | tar | create/extract tarballs off the main thread; `extract()`/`files()` reject on a truncated archive [v1.4.4] — docs <https://bun.com/docs/runtime/archive> |
135
+ | `new HTMLRewriter().on("a[href]", { element(el) { el.getAttribute("href") } }).transform(html \| response)` | cheerio, jsdom (scraping) | streaming CSS-selector handlers: `element`, `text`, `comments`, `onDocument`; rewrites `Response` bodies in flight — docs <https://bun.com/docs/runtime/html-rewriter> |
136
+ | `new Bun.Glob("**/!(*.test).ts").scan({ cwd })` | fast-glob, minimatch | async iterator of matches; `.match(path)`; extglob and `{1..3}` since 1.4.4 |
137
137
 
138
138
  ## Terminal text utilities (replaces string-width, slice-ansi, cli-truncate, wrap-ansi)
139
139
 
@@ -145,10 +145,35 @@ Bun.sliceAnsi(str, 0, 20); // slice by columns, preserving ANSI codes
145
145
  Bun.wrapAnsi(str, 80); // wrap by columns
146
146
  ```
147
147
 
148
+ ## Databases and object storage
149
+
150
+ Docs: <https://bun.com/docs/runtime/sql>, <https://bun.com/docs/runtime/sqlite>, <https://bun.com/docs/runtime/redis>, <https://bun.com/docs/runtime/s3>
151
+
152
+ ```ts
153
+ import { SQL } from "bun";
154
+ const sql = new SQL({ transform: SQL.camel }); // first_name <-> firstName both ways [v1.4.4]
155
+ const rows = await sql`SELECT * FROM users WHERE id = ANY(${[1, 2, 3]})`; // JS arrays bind to PG arrays [v1.4.4]
156
+ await Bun.file("users.csv").stream().pipeTo(sql`COPY users FROM STDIN (FORMAT csv)`.writable()); // 255 MiB/s, flat RSS [v1.4.4]
157
+ const q = sql`SELECT pg_sleep(30)`.execute(); setTimeout(() => q.cancel(), 100); // real server-side cancel [v1.4.4]
158
+
159
+ import { Database } from "bun:sqlite";
160
+ const db = new Database(":memory:");
161
+ db.function("add2", (a, b) => a + b); db.aggregate("sum_all", { start: 0, step: (t, v) => t + v }); // UDFs [v1.4.4]
162
+ db.onInsert = (db, table, rowid) => {}; // update hooks [v1.4.4]
163
+ db.query("SELECT title FROM movies").as(Movie).all(); // rows as class instances; .serialize()/Database.deserialize() for in-memory copies
164
+ ```
165
+
166
+ - `Bun.SQL` speaks PostgreSQL, MySQL/MariaDB and SQLite with one API; 1.4.4 cut client CPU per query 21-48%, added `result.columns`/`result.statement`, `sql.json(value)`, `channelBinding`, libpq URL params, TCP keepalive, and `bun --hot` reuses the pool.
167
+ - **Session traps:** `sql.unsafe()` and plain `` sql`` `` queries are lazy — `expect(query).rejects` never starts them and the test hangs (2026-10-13); `await` or `.execute()` first. A Postgres behind a TLS-terminating proxy lies in `pg_stat_ssl`; demand TLS on the client (see [security](security.md)). Bun 1.4.0 `node:sqlite` leaked the file handle after `close()` when `prepare()` was called (Windows EBUSY) — fixed in 1.4.4.
168
+ - `Bun.RedisClient` (`redis://`, `rediss://` with hostname verification): strings, hashes, sets, pub/sub, pipelining, auto-reconnect; replaces ioredis for the common path.
169
+ - `Bun.S3Client` / `Bun.s3` / `fetch("s3://bucket/key")` / `Bun.file("s3://…")`: AWS S3, Cloudflare R2, GCS, MinIO, Spaces; presigned URLs, streaming multipart writes, static `S3Client.list/stat/delete`. **Measured on our R2 mirror:** `writer().end(error)` COMPLETED a truncated multipart upload on 1.4.2 (oven-sh/bun#43134), a failed GET can end as an empty successful stream, and single-object PUT/DELETE returned `InternalError` where a hand-signed request succeeded (2026-10-08, 10-26) — verify size and hash after every S3 write, and abort on a zero-byte read of a non-empty object. Byte-equal copies can still lose HTTP metadata; copy headers explicitly.
170
+
148
171
  ## Everything else new in the runtime
149
172
 
173
+ - **`Bun.ModuleGraph`** (experimental) [v1.4.3]: run many instances of one app in one process, each with fresh module state, `require.cache`, timers and I/O; `graph.import()`, `graph.run(fn)`, `graph.dispose()` (closes its servers/sockets/children; not a security sandbox). Bytecode is shared between graphs.
174
+ - **`crypto.argon2()` / `argon2Sync()`** [v1.4.1]: argon2d/i/id byte-identical to Node; `Bun.password.hash()` for the high-level API.
150
175
  - **`URLPattern`**: the Web API, 408 WPT passing. Replaces path-to-regexp.
151
- - **`CompressionStream` / `DecompressionStream`**: gzip, deflate, deflate-raw + brotli and zstd.
176
+ - **`CompressionStream` / `DecompressionStream`**: gzip, deflate, deflate-raw + brotli and zstd; `new CompressionStream("brotli", { level: 4 })` trades ratio for speed (brotli defaults to 11, which is slow on large dynamic bodies) [v1.4.3].
152
177
  - **`Response.textStream()`**: `ReadableStream<string>` of the body decoded as UTF-8.
153
178
  - **`process.on("memoryPressure")`**: OS low-memory notification on macOS/Linux/Windows.
154
179
  - **ML-DSA and ML-KEM**: NIST post-quantum signatures/KEM in `crypto.subtle` and `node:crypto`.
@@ -0,0 +1,74 @@
1
+ # Bun 1.4.x Security Tightening
2
+
3
+ Blog: [#security](https://bun.com/blog/bun-v1.4#security), [1.4.3 #security](https://bun.com/blog/bun-v1.4.3), [1.4.4 #security](https://bun.com/blog/bun-v1.4.4)
4
+
5
+ 1.4.x tightened TLS/parsing defaults release by release. **A connection that worked before can now fail with a verification error — that is usually the fix working, not a bug.** Diagnose before loosening.
6
+
7
+ ## Running code you do not fully trust
8
+
9
+ ```bash
10
+ bun --disallow-code-generation-from-strings=strict --no-ffi-cc ./script.ts # [v1.4.1, strict tightened v1.4.4]
11
+ bun --config=/path/to/empty.toml --tsconfig-override=/path/to/empty.json ./trusted-gate.ts # trusted script, untrusted cwd
12
+ ```
13
+
14
+ - `=strict` refuses to run any code that is not a Bun builtin or one of the program's own files: `eval`, `new Function`, `node:vm`, `data:` imports, `Worker(code, { eval })`, `module._compile`, plugins that return source text, macros; in a `--compile` binary even `.ts/.js` files on disk, including ones the program just wrote. Workers inherit it; `BUN_OPTIONS` can raise but not lower it. Node's plain flag (no `=strict`) blocks only `eval`/`new Function`.
15
+ - `--no-ffi-cc` (also implied by `--no-addons`) makes `cc()` from `bun:ffi` throw `ERR_FFI_CC_DISABLED` — no runtime C compilation from untrusted JS [v1.4.1].
16
+ - **An untrusted checkout as cwd hijacks a trusted script**: its `bunfig.toml` `preload` runs first and its `tsconfig.json` `paths` rewrite even the script's relative imports (measured 1.4.4, 2026-10-26). Point `--config` and `--tsconfig-override` at empty files outside the checkout.
17
+ - `globalThis.Bun`, `Bun.fetch` and `Bun.plugin` are non-writable; `Bun.fetch(file:URL).blob()` reads a file without going through a replaced `Bun.file` or global `fetch`, so an in-process read guard is not a sandbox — enforce at the OS boundary (2026-10-27). `Bun.Glob` can silently return no-match under folded-wildcard backtracking; a deny matcher built on it must prove fail-closed behaviour (2026-10-20).
18
+
19
+ ## TLS verification got stricter in every 1.4.x [v1.4.1, v1.4.3, v1.4.4]
20
+
21
+ - `fetch()` verifies the certificate against the **URL host**, not a custom `Host` header (a proxy passing user-controlled `Host` was an insecure default); set `tls.servername` to verify an IP connection against another name [v1.4.1].
22
+ - `fetch`, `WebSocket`, `Bun.connect`, `RedisClient` and `Bun.SQL` verify during the handshake, not after; name matching accepts only valid host names and canonical IPs (CVE-2026-48618) [v1.4.3].
23
+ - Servers (`Bun.serve`, `Bun.listen`, `node:tls`) request a client certificate only with `requestCert: true`; setting `ca` alone no longer does — for mTLS set both [v1.4.4].
24
+ - `Bun.SQL` treats `tls: Bun.file("ca.pem")` as `verify-full`; a `tls.checkServerIdentity` upgrades any weaker `sslmode` to `verify-full` [v1.4.3/1.4.4]. A managed Postgres behind a TLS-terminating proxy reports SSL on the server while the client link is plain — demand TLS on the client with an explicit `tls` option (2026-10-03). `Bun.SQL` no longer reads `PASSWORD`; use `PGPASSWORD`/`MYSQL_PASSWORD` or the URL [v1.4.4].
25
+ - `--use-system-ca` / `NODE_USE_SYSTEM_CA=1` imports the OS store into BoringSSL for fetch/WebSocket (also compiled binaries via `--compile-exec-argv` or the env at spawn), but it widens trust unsafely on all three OSes (Windows ignores Disallowed, macOS falls back to BasicX509, Linux sweeps deselected CAs); prefer an explicit `ca` or `NODE_EXTRA_CA_CERTS`, which must be set before the process starts (measured 1.4.4, 2026-10-26).
26
+
27
+ ## checkServerIdentity runs BEFORE fetch() sends [v1.4.0]
28
+
29
+ ```ts
30
+ await fetch("https://api.example.com/upload", {
31
+ method: "POST",
32
+ body: secretPayload,
33
+ tls: {
34
+ checkServerIdentity(hostname, cert) {
35
+ if (cert.fingerprint256 !== PINNED) return new Error("pin mismatch");
36
+ },
37
+ },
38
+ });
39
+ // nothing is sent until the callback returns undefined; re-runs on each redirect hop
40
+ ```
41
+
42
+ If you pin a cert and the URL redirects through another host, the callback sees that cert too — accept every hop or use `redirect: "manual"`.
43
+
44
+ ## tls.connect defaults servername to host [v1.3.13]
45
+
46
+ `tls.connect({ host, port })` without `servername` uses `host` for SNI + identity check (matches Node). Connecting by IP or `localhost` to a cert issued for another name now fails with `ERR_TLS_CERT_ALTNAME_INVALID` — including through drivers like `pg`/`ioredis`. Fix: pass the cert's name as `servername`, or `checkServerIdentity: () => undefined` if trusting by CA alone.
47
+
48
+ ```ts
49
+ tls.connect({ host: "10.0.0.12", port: 5432, ca, servername: "db.internal" });
50
+ ```
51
+
52
+ ## Bun.connect / Bun.listen enforce rejectUnauthorized [v1.4.0]
53
+
54
+ `Bun.connect({ tls })`, `socket.upgradeTLS()`, and `Bun.listen()` with `requestCert: true` default to `rejectUnauthorized: true`. A self-signed dev server with no `ca` does NOT throw — the `handshake` handler runs with `socket.authorized === false`, writes return `-1`, and the socket closes without data. Pass the CA in `tls`, or `rejectUnauthorized: false` (`NODE_TLS_REJECT_UNAUTHORIZED=0` honored).
55
+
56
+ ## RedisClient verifies TLS hostname [v1.3.14]
57
+
58
+ `rediss://` checks the server cert against the URL host; first command rejects with `ERR_TLS_CERT_ALTNAME_INVALID` on mismatch. Connect by the certificate's name, or pass `tls: { rejectUnauthorized: false }`.
59
+
60
+ ## HTTP request parsing hardening in Bun.serve [v1.3.4]
61
+
62
+ More malformed `Content-Length` / `Transfer-Encoding` / chunked bodies get `400` + connection close, without calling your `fetch` handler or logging. Browsers/curl/proxies never send these; if a hand-written client starts seeing 400s, fix its framing headers.
63
+
64
+ ## Tarball extraction hardening [v1.3.6]
65
+
66
+ `github:`/URL dependencies and `bun create` templates skip tar entries that would land outside the package directory. A post-upgrade `Cannot find module` for such a package usually means the repo has a symlink pointing outside the package — replace it with a real file or relative link.
67
+
68
+ ## Also
69
+
70
+ - Registry credentials stay scoped to their configured host: never sent cross-origin, never downgraded to `http://`, never printed in errors/verbose output; `bun install` parses registry/tarball URLs strictly before attaching credentials [v1.4.3].
71
+ - `tls.createServer({ requestCert: true })` now rejects unverified client certs by default; only a literal `rejectUnauthorized: false` disables verification (`null` no longer counts).
72
+ - `fetch()` rejects a `Content-Length` that disagrees with a streamed body, chunked bodies with bare-LF line endings, and `file://host/path` with a non-local host [v1.4.3].
73
+ - Secrets in a session: a child spawned without `env` inherits the STARTUP environment including every secret set at startup — pass an explicit minimal `env` to any child that must not see one; `delete process.env.X` does not reach such children (2026-10-20/22). Low file descriptors are not free under Bun: hand a secret to a child over stdin or a documented high fd (2026-10-19). `Bun.secrets` keeps tokens in the OS keychain (see [session-recipes](session-recipes.md)).
74
+ - Full list: [Security hardening](https://bun.com/blog/bun-v1.4#security-hardening) in the changelog; advisories at <https://github.com/oven-sh/bun/security/advisories>.
@@ -0,0 +1,71 @@
1
+ # Session recipes — Bun.$, Bun.spawn, secrets, debugging
2
+
3
+ Every trap here was paid for by a past agent session (dates are the memory notes). Read the trap, apply the one-line fix.
4
+
5
+ ## Bun.$ — a bash-like shell parsed by Bun, not `/bin/sh`
6
+
7
+ Docs: <https://bun.com/docs/runtime/shell>
8
+
9
+ ```ts
10
+ const { stdout, exitCode } = await $`git status --porcelain`.nothrow().quiet(); // never throws, no echo
11
+ const text = await $`ls`.text(); const pr = await $`gh pr view 1 --json state`.json();
12
+ for await (const line of $`cat big.log`.lines()) {}
13
+ await $`bun run build`.cwd("/repo").env({ ...process.env, CI: "1" });
14
+ ```
15
+
16
+ | Trap | What happens | Fix |
17
+ |---|---|---|
18
+ | `${"a b c"}` | ONE argv entry — vitest reported "No test files found" with rc 0 and the gate looked green (2026-09-21) | pass an array: `${["a", "b", "c"]}` spreads into separate args |
19
+ | `$?`, `$VAR`, `for … done`, `$(…)` | `$` is escaped to a literal; unsupported syntax is a PARSE error (nothing runs) or a silent `command not found` with rc 0 (2026-10-10, 10-22) | run real bash: `Bun.spawn(["bash", "-c", script])` or `` $`bash -c ${script}` `` |
20
+ | `VAR=x cmd` prefix | not applied to a directly executed binary — `` $`HOME=/x printenv HOME` `` prints the real HOME; only `sh -c` sees it (2026-10-25) | `.env({ ... })`, or `Bun.spawn(cmd, { env })` |
21
+ | `ssh host <<'EOF' … EOF` | the heredoc body ran on the LOCAL host (2026-10-23) | `Bun.spawn(["ssh", host, "bash -s"], { stdin: new Blob([script]) })`, or scp a script file |
22
+ | Globs in `${...}` | literal since 1.4.0; only patterns written in the template expand | write the pattern in the template |
23
+ | NUL byte in an env value | value truncated; `.env()`/`.cwd()` throw `ERR_INVALID_ARG_VALUE` [v1.4.4] | strip NULs |
24
+
25
+ Builtins (`cd ls rm echo pwd cat touch mkdir which mv seq dirname basename`) run in-process, so `cd` persists across `;` within one template. `$.escape(str)` shell-quotes a string; `$.braces("a{b,c}")` expands braces; `bun run ./script.sh` runs a `.sh` file through this shell on every OS.
26
+
27
+ ## Bun.spawn — the child gets the STARTUP env, not `process.env`
28
+
29
+ `Bun.spawn`/`spawnSync` without `env` give the child the environment the process started with (documented in bun-types; measured on 1.4.3 and 1.4.4). A `process.env.X = …` set at runtime — hermetic HOME/TMPDIR from a test preload — never reaches an env-less child; `node:child_process` differs (omo#11259, 2026-10-25). Always pass `env: process.env` or an explicit object.
30
+
31
+ ```ts
32
+ const proc = Bun.spawn(["bash", "-c", script], {
33
+ env: process.env, cwd, stdout: "pipe", stderr: "pipe", stdin: "ignore",
34
+ timeout: 60_000, killSignal: "SIGKILL", // the child dies, not the eval cell
35
+ onExit(p, code, signal) {},
36
+ });
37
+ const [out, err] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
38
+ const code = await proc.exited;
39
+ proc.resourceUsage(); // { maxRSS, cpuTime: { user, system } } — measure, do not guess
40
+ ```
41
+
42
+ - `stdin: "ignore"` (or write then `.end()` the `FileSink`) for any child that may read stdin: a `bun -e` program reading stdin, or `promisify(execFile)` with `input` (Bun ignores it), waits for EOF forever; under ssh it orphans and blocks later jobs (2026-08-31, 2026-10-30).
43
+ - Re-spawning Bun: use `process.execPath`, never `"bun"` from PATH — a mise shim refused a temp HOME, a stray 1.4.0 shim in `~/.bun/install/global/node_modules/.bin` hung rpc tests, and after a reboot PATH resolved `bun` to a vite-plus shim with shim recursion; the `bun test vX.Y.Z` header line is the tell (2026-09-28, 10-26, 10-29). Inside a compiled binary `bun` can be the binary itself — see [build](build.md).
44
+ - `proc.signalCode` is set when a signal is SENT, not when the child exits: a child ignoring SIGTERM keeps running while `signalCode === "SIGTERM"`; await `proc.exited` with a deadline, then `proc.kill("SIGKILL")` (2026-10-02).
45
+ - **The js eval kernel SIGKILLs every `Bun.spawn` child when its cell settles** — four Chromium launches in a row died 0.05-9 s after the launching cell completed, and a child parked in `globalThis` does not survive (2026-09-16/10-17). Anything that must outlive a cell (browser, daemon, dev server, `ssh` running a remote job, a `git commit` with slow hooks) starts through `tool.monitor({ command })` or `tool.bash({ run_in_background: true })`; keep only its id/pid in `globalThis`. Conversely `bun --no-orphans script.ts` makes a script exit when its parent dies and SIGKILL every descendant on exit. A stopped long `Bun.$` cell restarts the whole kernel and loses every global; keep helpers in an importable file and restore with `Object.assign(globalThis, await import(path))` (2026-10-02).
46
+ - PTY: `Bun.spawn(["bash"], { terminal: { cols, rows, data(term, chunk) {} } })` then `proc.terminal.write()/resize()` — replaces node-pty and the macOS `script -q /dev/null` trick.
47
+ - `Bun.spawnSync` blocks the kernel thread and cannot be interrupted: never in an eval cell. `maxBuffer` kills a chatty sync child.
48
+ - `signal: AbortSignal.timeout(ms)` works for `fetch` and `spawn` alike (`killSignal` picks the signal).
49
+
50
+ ## Secrets and env
51
+
52
+ - `Bun.secrets.get({ service, name })` / `.set({ service, name, value })` / `.delete()` — macOS Keychain, Linux libsecret, Windows Credential Manager (`persist: "local"` keeps it on one machine [v1.4.3]). A token read this way never sits in a file, argv, or an env dump; `null` when missing. Docs: <https://bun.com/docs/runtime/secrets>
53
+ - `bun --env-file=<(op read …) app.ts` or `echo A=1 | bun --env-file=/dev/stdin app.ts` [v1.4.1]: env from a pipe/FIFO, no `.env` on disk. `--no-env-file` (or `env = false` in bunfig) skips automatic `.env` loading in CI/prod; `bun --bun` and a `node` symlink never auto-load `.env`.
54
+ - `BUN_OPTIONS="--preload ./shim.ts"` injects flags into every `bun` invocation, including `bun -e`, `bun test` and `--compile` binaries (it can raise but not lower `--disallow-code-generation-from-strings`).
55
+ - Corporate CAs: `NODE_EXTRA_CA_CERTS=/path/ca.pem` reaches every fetch/WS/Worker/node:https path when set before the process starts (assigning it from JS does nothing); `--use-system-ca` also works in compiled binaries but widens trust — [security](security.md) has the measured gaps (2026-10-26).
56
+
57
+ ## Debugging and measuring inside a session
58
+
59
+ - `BUN_CONFIG_VERBOSE_FETCH=curl` prints every `fetch()`/`node:http` request as a copy-pasteable `curl` line plus request/response headers; `fetch(url, { verbose: true })` for one call.
60
+ - `process.memoryUsage().rss` on macOS matches Activity Monitor and `process.resourceUsage().maxRSS` its peak [v1.4.3]; `Bun.gc(true)` returns freed memory to the OS before returning, so RSS drops immediately [v1.4.3]; `estimateShallowMemoryUsageOf(obj)` from `bun:jsc` per object; `Bun.generateHeapSnapshot()` for DevTools; `bun --smol` / `new Worker(f, { smol: true })` on memory-constrained hosts.
61
+ - `Bun.nanoseconds()` monotonic since process start; `Bun.sleep(ms)`; `Bun.peek(promise)` reads an already-settled promise without a microtick.
62
+ - One-liners: `bun -p 'Bun.version'` prints an expression, `bun -e` runs code, `bun exec 'cmd | wc -l'` runs a Bun-shell string, `bun ./README.md` renders Markdown in the terminal.
63
+ - Kernel vs shell Bun: senpi bundles its own Bun, so the kernel's `Bun.version` can lag the shell's `bun --version` on one machine (1.4.3 vs 1.4.4 on 2026-10-31) — a newer-only feature such as `Bun.Glob` extglob returned false in the kernel and worked in a spawned `bun`. Check the version of the Bun that will run the code.
64
+
65
+ ## Files, globs, streams
66
+
67
+ - `Bun.file(p).exists()`, `.size`, `.type`, `.text()/.json()/.bytes()/.stream()`, `file.delete()`; `Bun.write(path, data | Response | ReadableStream | Bun.file(other))` streams to disk (a 128 MiB download adds 13 MB of RSS, not 161 MB [v1.4.1]) and doubles as `cp`.
68
+ - **Two writes that destroyed data:** `Bun.file(p).writer({ append: true })` truncated an NDJSON results file at open and overwrote 927 rows (2026-10-20) — append incremental results with `node:fs` `appendFileSync` per batch, or write per-batch part files; `Bun.write(p, prefix + await Bun.file(p).text())` left a 0-byte file because the lazy handle was truncated before it was read (2026-10-30) — read into a variable, then write. Check `wc -c` after any rewrite of a file you will hand to someone else.
69
+ - `new Bun.Glob("**/*.ts").scan({ cwd, dot, onlyFiles })` async-iterates matches; `.match(path)`; `!(…)`, `@(…)`, `{1..3}` from 1.4.4 (a pattern containing `(` changed meaning — see breaking-changes).
70
+ - `Bun.readableStreamToText/JSON/Array/Blob(stream)`; `for await (const line of console)` reads stdin line by line; `Bun.stdin.stream()`.
71
+ - `Bun.resolveSync("pkg", import.meta.dir)`, `import.meta.dir/file/path/main`, `Bun.main` (entrypoint path), `Bun.which("gh")`.