bun#43660 reported that in a cp932 console, the output of Bun | Bun is garbled and 65001 is left behind after exit. That was one symptom.
Bun rewrites the console input and output code pages while it runs. Because the code page is shared by every process in the same console, the effect reaches beyond Bun itself.
✅ observed / ⬜ not yet measured.
| Harm | Evidence | Status |
|---|---|---|
| H1 | The output of a piped peer is garbled (the bun#43660 symptom). Research with 1.4.2 and canary, T2, T5 | ✅ observed |
| H2 | The result changes from run to run, decided by the start and exit order. The three research runs, T5 | ✅ observed |
| H3 | 65001 is left on the console after exit. Research, T1, T5 | ✅ observed |
| H4 | Other processes in the same console are affected: cmd misreads its Shift_JIS script, and messages such as pause switch to English. Research, T4 | ✅ observed |
| H5 | After another Bun process has put the console back to 932, Bun writes raw UTF-8 bytes and they are garbled (it does not use WriteConsoleW). Research, 1.4.2 run 3, T5 | ✅ observed |
| H6 | A forced kill leaves the code page changed. T3 | ✅ observed |
| H7 | It happens even when the two Bun processes are not directly connected (bun | node | bun). T5 | ✅ observed |
The observed results are in Code page test results and Reproducing bun#43660. The cause of H2 (decided by the start and exit order) was confirmed in T5.
Do not change the console code page. Writing to the console with WriteConsoleW in UTF-16, and writing bytes unchanged to pipes and files, does not depend on the code page ("Expected behavior"). Node.js and Deno working correctly without changing it shows this is feasible (T1, T2).
If the code page is not changed, none of H1 to H7 happens. The bun#43660 symptom (H1) is among them.
A freshly opened cmd window on Japanese Windows (code page 932). The tests never change the code page themselves.
conhost.exe and runs inside it. Results are written to files and read back. conhost.exe is named explicitly because, when Windows Terminal is the default terminal, a new console may also end up under ConPTY (not yet verified). bun#43660 was also reported with cmd.exe used directly, not Windows Terminal.| Target | How to write / read |
|---|---|
| Output to a console | Pass UTF-16 to WriteConsoleW |
| Output to a pipe or file | Write the UTF-8 bytes as they are |
| Input from a console | Read UTF-16 with ReadConsoleW |
| Input from a pipe | Take the bytes as they arrive |
None of these depends on the code page, so there is no reason to change it. T1 confirmed that Node.js and Deno print Japanese correctly to a 932 console without ever changing the code page (whether they use WriteConsoleW internally was not examined).
| Reported | Here |
|---|---|
| When the starting code page is not 65001, Bun piped to Bun produces garbled output | Reproduced (both stable and canary) |
| It does not happen if either side is Node.js | Confirmed (T2: if either side is Node.js or Deno, 65001 is not left) |
| 65001 is left on the console after exit | Reproduced, though in some runs it was not left |
| Runtime | Version | Role |
|---|---|---|
| Bun | 1.4.3+c6da4a4d3 | Under test (stable) |
| Bun canary | latest at setup time (1.4.3-canary.1+bbdc5a519 in the full run) | Under test. Placed in _bin/bun-canary/ and called with PATH switched |
| Bun PR #43662 | 1.4.3-canary.1+6b5271cb1 | The build of the PR that fixes bun#43660, to see what it fixes and what remains. Placed in _bin/bun-pr-43662/ and called with PATH switched |
| Node.js | v26.10.0 | Reference |
| Deno | 2.9.7 | Reference |
An implementation that follows the expected behavior gives the following for every case.
| Case | Harm shown | Expected |
|---|---|---|
| T1 | H3 | Both input and output stay at 932 before, during and after the run |
| T2 | H1 | In all 27 cases the reader receives the UTF-8 bytes unchanged (cases via Windows PowerShell 5.1 match Node.js | Node.js) |
| T3 | H6 | Still 932 after a forced kill |
| T4 | H4 | The Shift_JIS cmd script prints Japanese correctly |
| T5 | H1, H2, H3, H7 | Whatever the start and exit order, the screen shows abc 東京大阪 xyz correctly and the console stays at 932 afterwards |
| T6 | All | Both the stable and the canary build meet the expectations of T1 to T5 |
In the baseline console, record GetConsoleCP / GetConsoleOutputCP before, during and after the run. Each runtime reads the values during the run through its own FFI (read only): bun:ffi for Bun, koffi for Node.js, Deno.dlopen for Deno. Expected: 932 at every point.
| Axis | Values |
|---|---|
| Writer | Bun / Node.js / Deno |
| Reader | Bun / Node.js / Deno |
| Launched from | cmd / pwsh 7 / Windows PowerShell 5.1 (all inside the baseline console) |
3 × 3 × 3 = 27 cases. The reader writes the received bytes to a file unchanged, and they are compared with the expected UTF-8 bytes. Nothing is judged by how the screen looks.
? when piping between native commands, even without Bun. For 5.1, Node.js | Node.js is the baseline and only differences specific to Bun are counted.Kill a waiting Bun process with taskkill /F and check that the console is back at 932.
Right after Bun | Bun (normal exit and forced kill), check that a cmd script saved in Shift_JIS still prints Japanese correctly.
Fix the order in which two or more processes start and exit by inserting delays, so the result does not depend on chance. Each process writes GetConsoleCP / GetConsoleOutputCP with a timestamp to a log right after start, right before writing, and right before exit (read only). The text sent is abc 東京大阪 xyz ("Tokyo Osaka"). It has an even number of characters (4 characters, 12 bytes), so when read as CP932 every 2 bytes become another character and no broken byte is left over. Garbled, it becomes abc 譚ア莠ャ螟ァ髦ェ xyz: the surrounding ASCII and the newline survive, and the correct and garbled forms can be told apart mechanically (computed with Node.js TextDecoder('shift_jis'); not yet checked against the console display).
| Shape | How the order is fixed | What it shows |
|---|---|---|
bun | bun | Start order: ping -n 2 127.0.0.1 >nul before one side starts. Exit order: setTimeout in the script | Start order × exit order × writing to the console or not |
bun | node wait | bun | The Node.js process in the middle holds the data for a set time before passing it on | Whether it happens even when the two Bun processes are not directly connected (H7) |
node | node wait | node | Same | Reference |
deno | node wait | deno | Same | Reference |
| Same shape as bun#43660 | No fixed order (both started together with -e). Run 10 times | Whether the result changes from run to run (H2) |
| ww with other write APIs | The ww order, with the reader writing to the screen with fs.writeSync(1), fs.write(1) or Bun.write(Bun.stdout). Bun builds only | Whether the write APIs PR #43662 does not route through WriteConsoleW are garbled |
Expected results for bun | bun under the hypothesis (each Bun process saves the current value and sets 65001 at start, and restores the saved value at exit):
| Starts first | Exits first | Screen | After exit |
|---|---|---|---|
| Writer | Writer | Garbled | 65001 left behind |
| Writer | Reader | Correct | Back to 932 |
| Reader | Writer | Correct | Back to 932 |
| Reader | Reader | Correct | 65001 left behind |
Each combination runs three times, and the results are listed in run order. For the background, see Reproducing bun#43660.
Run T1 to T5 with both the stable and the canary build of Bun. Node.js and Deno stay at one recorded version as the reference.
tests/
codepage.test.ts T1 to T5 (node:test, also run with bun test). Bun and canary are columns of the same table, which is T6
fixtures/
console.ts shared read-only code page helpers (bun:ffi / koffi / Deno.dlopen)
emit.ts writer that prints abc 東京大阪 xyz with console.log
sink.ts reader that writes the received bytes unchanged to the screen or a file
hold.ts relay that holds the data for a set time before passing it on (node wait in T5)
probe.ts observer (Node.js): records the code page and reads the characters on the console screen
neighbor-932.cmd neighbor process for 932 (Shift_JIS + CRLF)
neighbor-437.cmd neighbor process for 437 (CP437 + CRLF)
harness/
new-console.ts creates a new console with conhost.exe, runs a case inside it and waits for it to end
judge.ts judges the screen, bytes and code page, and records them in results.jsonl in the evidence folder
manual/
issue-43660-repro.cmd runs the same shape of command as bun#43660 by hand (screen checked by eye)
tools/10_setup/
setup-runtimes.ps1 / .cmd setup phase: downloads the runtimes into _bin/ and runs npm ci
fetch-bun-pr.ts downloads the Windows x64 build of a Bun PR from Bun's CI (Buildkite)
tools/40_test/
run-tests.ps1 / run-tests.cmd test phase: runs node --test and bun test at 932 and 437
tools/50_run/
summarize-results.ts prints the material for the result tables from the evidence folder
compare-evidence.ts compares the verdicts in research/evidence_last/ and research/evidence/
build-result-tables.ts builds the result tables from research/evidence/, linking to the evidence files and the test code
verify-issue-repro.ts runs the reproduction command in the issue text in new consoles and prints the screen
.ts file that runs on Bun, Node.js and Denotmp/timeoutReadConsoleOutputCharacterW. No one needs to look at itAnyone who clones the repository on Windows can run the same tests. Nothing depends on the Bun, Node.js or Deno installed on that PC.
tools/10_setup/setup-runtimes.cmd (backed by the ps1 of the same name) downloads pinned versions from the official sources and places them under _bin/, which is not tracked by Git. Anything already in place is not downloaded again.
| Runtime | Version | Location |
|---|---|---|
| Bun | 1.4.3 | _bin/bun/ |
| Bun canary | latest at setup time (it cannot be pinned, so the version is recorded) | _bin/bun-canary/ |
| Node.js | v26.10.0 | _bin/node/ |
| Deno | 2.9.7 | _bin/deno/ |
| pwsh | 7.6.6 | _bin/pwsh/ |
| Bun PR #43662 | build of the PR head commit | _bin/bun-pr-43662/. The artifact from Bun's CI (Buildkite), fetched by tools/10_setup/fetch-bun-pr.ts and checked against its SHA-1 |
Then npm ci runs with the npm in _bin/node/ (for koffi and type checking).
tools/40_test/run-tests.cmd puts the downloaded runtimes on PATH with set PATH=…\_bin\…;%PATH%where confirms that bun, node, deno and pwsh resolve to the ones under _bin/. Otherwise it stopsbun --revision, node -v, deno --version, pwsh) is written to versions.txtrun.cmd) calls the runtimes by name only, and every path is relative to %~dp0. No path containing a user name appears anywherebun, so they run in separate passes with PATH switched. The canary pass runs only the cases that involve Bunchcp first (the console is not shared with anything else). Verdicts are relative to the starting valueneighbor: 東京大阪 in Shift_JIS) and one for 437 (neighbor: café in CP437)environment.json with the starting code page, tool names, tool versions and the run time in UTC Z format (for example 2026-10-07T03:45:12Z). This is the only file with a date| Location | Handling |
|---|---|
research/evidence_last/ | Overwritten on every run. Not excluded by Git, but not committed |
research/evidence/ | Committed as the baseline. Copied from evidence_last after it is checked. After a re-test, it is copied without waiting to be told (local rules) |
versions.txt, the result records (for node --test and bun test), and per case the run.cmd, logs, the screen read back, and the bytes the reader receivedtools/50_run/compare-evidence.ts compares the two and shows only differences in the verdicts (✅ / ❌), not in timingsresearch/evidence/research/evidence/ (done: every test ran at 932 and 437, the results were copied to research/evidence/, and every ✅ / ❌ in the result tables links to its evidence file)node tools/80_ops/check-replies.ts. It prints only what is new since the last check and leaves out the posting account. The time of the last check is kept in etc/check-replies.json (not in Git)releases/latest differs from the last check, it is printed as new. Canary is left out because it changes every day. When a new release appears, re-run the tests, update the documents and push (local rules, "Issues and publishing"). The same goes when the head commit of PR #43662 changes✅mm/dd hh:mm reply check: nothing new)