bun:windows exposes Windows interfaces as synchronous functions: the registry, services, the event log, the clipboard, known folders, processes, Job Objects, toast notifications and WSL. The module loads on first import, so it costs nothing at startup.
import windows from "bun:windows";
if (windows.isSupported) {
console.log(windows.version().productName); // "Windows 11 Pro"
}
On other platforms isSupported is false. Every function exists, and each call throws an error with code ERR_BUN_WINDOWS_UNSUPPORTED.
Errors
A failed Windows call throws a SystemError with code (such as "EACCES"), syscall (the Win32 function) and winError, the raw GetLastError or HRESULT value. The message includes the system text for the error.
try {
windows.services.stop("EventLog");
} catch (error) {
// Without elevation: error.winError is 5 (ERROR_ACCESS_DENIED)
console.log(error.code, error.syscall, error.winError);
}
Invalid arguments throw TypeError or RangeError before any Windows call runs.
System
const v = windows.version();
v.build; // 26100
v.displayVersion; // "24H2"
v.isWindows11; // true
windows.isWindows11(); // false on other platforms instead of throwing
windows.isElevated(); // true in an administrator process
const info = windows.systemInfo();
info.totalMemory; // bytes
info.theme; // "dark" | "light"
version() reads RtlGetVersion and the CurrentVersion registry key once. productName says Windows 11 on builds 22000 and later, where the registry still says Windows 10.
Registry
Paths start with a root (HKCR, HKCU, HKLM, HKU, HKCC, or the long HKEY_* names) and use \ or /.
const { registry } = windows;
registry.set("HKCU\\Software\\MyApp", "Theme", "dark"); // REG_SZ
registry.set("HKCU\\Software\\MyApp", "Launches", 3); // REG_DWORD
registry.set("HKCU\\Software\\MyApp", "Id", 2n ** 40n); // REG_QWORD
registry.set("HKCU\\Software\\MyApp", "Paths", ["C:\\a", "C:\\b"]); // REG_MULTI_SZ
registry.set("HKCU\\Software\\MyApp", "Path", "%USERPROFILE%\\x", "REG_EXPAND_SZ");
registry.get("HKCU\\Software\\MyApp", "Theme"); // { type: "REG_SZ", value: "dark" }
registry.get("HKCU\\Software\\MyApp", "Missing"); // null
registry.list("HKCU\\Software\\MyApp"); // { keys: [], values: [{ name, type, value }, ...] }
registry.deleteValue("HKCU\\Software\\MyApp", "Theme"); // true
registry.deleteKey("HKCU\\Software\\MyApp", { recursive: true }); // true
set creates missing keys. REG_QWORD values read as bigint and binary values as Buffer. Pass { view: "64" } or { view: "32" } to choose the registry view.
Services
windows.services.list(); // [{ name, displayName, state, type, pid }, ...]
windows.services.list({ drivers: true });
const svc = windows.services.get("Spooler");
svc?.state; // "running"
svc?.startType; // "auto"
windows.services.stop("Spooler"); // needs an elevated process
windows.services.start("Spooler");
get returns null for an unknown service. Starting a running service and stopping a stopped service are not errors.
Event log
const errors = windows.eventLog.query("System", {
xpath: "*[System[(Level=2)]]",
limit: 10,
});
// Array of event XML strings, newest first
windows.eventLog.write("MyApp", "Started", { type: "information", eventId: 1 });
write adds an event to the Application log. A source that is not registered still writes, and Event Viewer shows the message with a note about the missing source.
Clipboard
windows.clipboard.writeText("hello");
windows.clipboard.readText(); // "hello"
windows.clipboard.clear();
windows.clipboard.readText(); // null
Known folders
windows.knownFolder("Downloads"); // "C:\\Users\\me\\Downloads"
windows.knownFolder("ProgramData"); // "C:\\ProgramData"
windows.knownFolder("{FDD39AD0-238F-46AF-ADB4-6C85480369C7}"); // Documents
windows.knownFolders maps the accepted names to their KNOWNFOLDERID GUIDs. Any other GUID works too.
ConPTY
const info = windows.conpty.info();
info.supported; // true on Windows 10 1809+
info.releasePseudoConsole; // true on Windows 11 24H2+
info.closeBlocks; // true when ClosePseudoConsole waits for the output pipe to drain
const terminal = windows.conpty.open({ cols: 80, rows: 24, data(term, chunk) {} });
terminal.resize(100, 30);
terminal.close();
conpty.open() returns a Bun.Terminal. info() reads RtlGetVersion and checks kernel32 for ReleasePseudoConsole once.
Processes
windows.processes.list(); // [{ pid, ppid, name, threads }, ...]
windows.processes.path(process.pid); // full path of bun.exe
windows.processes.terminate(1234, 1);
A pid argument also accepts an object with a pid property, such as the Subprocess returned by Bun.spawn.
Job Objects
A Job Object groups processes under shared limits and accounting. Children of an assigned process join the job.
using job = new windows.Job({
killOnClose: true,
jobMemory: 512 * 1024 * 1024,
cpuRate: 50,
});
const child = Bun.spawn(["bun", "worker.ts"]);
job.assign(child);
job.info(); // { activeProcesses, totalProcesses, userTime, kernelTime, peakJobMemory, pids, ... }
job.terminate(1);
With killOnClose, closing the job (or leaving the using block) terminates its processes. A job that is garbage collected is closed too.
| Limit | Meaning |
|---|---|
killOnClose | Terminate the processes when the job closes |
processMemory | Committed memory per process, in bytes |
jobMemory | Committed memory of the whole job, in bytes |
activeProcesses | Maximum number of live processes |
cpuRate | Hard CPU cap, percentage of the machine |
Notifications
windows.notify("Build finished", "3 warnings");
windows.toast(`<toast><visual><binding template="ToastGeneric">
<text>Custom</text>
</binding></visual></toast>`);
Notifications are shown under an App User Model ID. The default is the one of Windows PowerShell, which every Windows install has. Pass { appId } to use the id of your own installed app.
WSL
windows.wsl.distributions(); // [{ id, name, basePath, version, default }, ...]
const { exitCode, stdout } = windows.wsl.run("Ubuntu", ["uname", "-r"]);
distributions reads the registry and does not start WSL. run calls wsl.exe -d <name> -- <command> and waits for it.
NTFS
Read-only access to the USN change journal and the Master File Table of an NTFS volume.
const { ntfs } = windows;
using vol = new ntfs.Volume("C"); // drive letter or "\\\\?\\Volume{guid}\\"
const journal = vol.journalQuery(); // { journalId, nextUsn, maxSize, ... }
vol.journalCreate(); // creates the journal if missing; no-op otherwise
const change = vol.journalRead(journal.nextUsn, { bufferSize: 64 * 1024 });
change.records; // [{ usn, record, parent, fileName, reason, timestamp, ... }, ...]
change.nextUsn; // pass back in to continue reading
const all = vol.enumerateMft(4 << 20); // every record, bounded by one read buffer size
all.find(r => r.fileName === "autoexec.bat");
record and parent are NTFS file reference numbers: the low 48 bits are the stable MFT record number, the high 16 bits are a reuse sequence. FSCTL_ENUM_USN_DATA does not return the reserved metadata files (record numbers 0-15, including the volume root); their attributes are still readable through ordinary path APIs. Opening a volume handle needs an elevated process.
PE, Authenticode and catalog signatures
Pure byte-level parsing of PE32/PE32+ images: headers, sections, imports, exports, resources, ApiSet redirection and Authenticode. Nothing here maps or executes the image — no LoadLibrary, no WinRT activation, no DllMain.
const { pe } = windows;
const bytes = new Uint8Array(await Bun.file("C:\\Windows\\System32\\kernel32.dll").arrayBuffer());
pe.looksLikePe(bytes); // true: cheap DOS/NT header check before a full parse
const info = pe.parse(bytes, "kernel32.dll");
info.kind; // "exe" | "dll" | "sys" | ...
info.machine; // "x86" | "x64" | "arm64" | "arm64ec"
info.sections; // [{ name, virtualAddress, virtualSize, rawSize, characteristics }, ...]
info.exports; // [{ name, ordinal, rva, forwarder }, ...]
info.imports; // [{ module, names, delayLoad }, ...]
info.authenticode; // { signer, issuer, digest, certificates } when the certificate table embeds a signature, else null
pe.authenticode.parse(bytes) parses a standalone PKCS#7 SignedData blob (the WIN_CERTIFICATE payload, or a .cat file) without needing a full PE image.
Most Windows system binaries carry no embedded signature and are listed in a system catalog instead:
const catalog = pe.catalogFile("C:\\Windows\\System32\\kernel32.dll"); // full path of the .cat file, or null
const signature = pe.catalogSignature("C:\\Windows\\System32\\kernel32.dll"); // { catalog, signer, issuer, digest } | null
pe.releaseCatalogContexts(); // releases the cached SHA-256/SHA-1 admin contexts; optional
A null result means "not listed in any catalog", never "unsigned" — a file can be both embedded-signed and catalog-listed, or signed through neither API and still be trusted by a mechanism this module does not cover. catalogFile/catalogSignature are slow (a catalog-database query): avoid them in a hot path over many files.
pe.parseApiSetSchema(bytes); // apisetschema.dll's `.apiset` section: contract -> host DLLs
pe.apiSetKey("api-ms-win-core-file-l1-2-4.dll"); // "api-ms-win-core-file-l1-2"
API families
The Bun binary contains the interfaces above. Other Windows API families ship as packages named @aphrody/bun-windows-<family>, and bun:windows loads them on first use.
const kernel32 = windows.family("kernel32"); // exports of @aphrody/bun-windows-kernel32
windows.families.user32; // same as windows.family("user32")
"gdi32" in windows.families; // true when the package is installed
The package is resolved from the working directory, then from the entry script, and loaded once per process. A family that is not installed throws an error with code ERR_BUN_WINDOWS_FAMILY_NOT_FOUND.
BUN_WINDOWS_FAMILY_PATH adds directories to search for bun-windows-<family> package folders (separated by ;).
Win32 functions, structs and COM
The families are generated from win32metadata by scripts/aphrody/win32/gen.ts: one package per DLL (368 families, about 18,000 functions), plus @aphrody/bun-windows-win32 with the struct, enum, constant and COM interface descriptions they share. A function is bound with bun:ffi the first time it is read.
const kernel32 = windows.family("kernel32");
kernel32.GetCurrentProcessId(); // 1234
kernel32.constants.PROCESS_QUERY_LIMITED_INFORMATION; // 0x1000
const { win32 } = windows;
const mem = win32.struct("MEMORYSTATUSEX"); // dwLength is filled in
kernel32.GlobalMemoryStatusEx(mem);
mem.ullAvailPhys; // bigint
Arguments are converted from their declared types:
- Strings are passed to
PWSTR,PSTRandBSTRparameters. - Plain objects are passed to struct pointers.
- Structs of 8 bytes or less are passed by value (
PtInRect(rect, { x, y })). - Functions are passed to callback parameters.
Functions that set the last error record it in win32.lastError().
COM classes are created by coclass name or CLSID. Methods are called through the vtable. [retval] parameters become the return value. A failing HRESULT throws a win32.Win32Error:
const service = win32.com.create("TaskScheduler", "System.TaskScheduler.ITaskService");
service.Connect(undefined, undefined, undefined, undefined);
service.GetFolder("\\").GetTasks(1).get_Count();
service.release();
win32.com.delegate(iid, handler) returns a COM callback object that any thread may call. Its reference count is atomic and lives in native code. handler runs on the JavaScript thread. Use it for completion and event callbacks that Windows invokes from its thread pool.