# Technical Documentation: felixrieseberg/windows95

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/felixrieseberg/windows95)

### Overview (`README.md`)

# windows95

This is Windows 95, running in an [Electron](https://electronjs.org/) app. Yes, it's the full thing. I'm sorry.

## Downloads

<table class="is-fullwidth">
</thead>
<tbody>
</tbody>
  <tr>
    <td>
      <img src="./.github/images/windows.png" width="24"><br />
      Windows
    </td>
    <td>
      <span>32-bit</span>
      <a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.1/windows95-5.0.1-setup-ia32.exe">
        💿 Installer
      </a> |
      <a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.1/windows95-win32-ia32-5.0.1.zip">
        📦 Standalone Zip
      </a>
      <br />
      <span>64-bit</span>
      <a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.1/windows95-5.0.1-setup-x64.exe">
        💿 Installer
      </a> |
      <a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.1/windows95-win32-x64-5.0.1.zip">
        📦 Standalone Zip
      </a><br />
      <span>
        ❓ Don't know what kind of chip you have? It's probably `x64`. To confirm, on your computer, hit Start, enter "processor" for info.
      </span>
    </td>
  </tr>
  <tr>
    <td>
      <img src="./.github/images/macos.png" width="24"><br />
      macOS
    </td>
    <td>
      <span>Apple Silicon Processor</span>
      <a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.1/windows95-darwin-arm64-5.0.1.zip">
        📦 Standalone Zip
      </a><br />
      <span>Intel Processor</span>
      <a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.1/windows95-darwin-x64-5.0.1.zip">
        📦 Standalone Zip
      </a>
      <span>
        ❓ Don't know what kind of chip you have? If you bought your computer after 2020, select "Apple Silicon". Learn more at <a href="https://support.apple.com/en-us/HT211814">apple.com</a>.
      </span>
    </td>
  </tr>
  <tr>
    <td>
      <img src="./.github/images/linux.png" width="24"><br />
      Linux
    </td>
    <td>
      <span>64-bit</span>
      <a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.1/windows95-5.0.1-1.x86_64.rpm">
        💿 rpm
      </a> |
      <a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.1/windows95_5.0.1_amd64.deb">
        💿 deb
      </a><br />
      <span>ARM64</span>
      <a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.1/windows95-5.0.1-1.arm64.rpm">
        💿 rpm
      </a> |
      <a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.1/windows95_5.0.1_arm64.deb">
        💿 deb
      </a><br />
      <span>ARM</span>
      <a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.1/windows95-5.0.1-1.armv7hl.rpm">
        💿 rpm
      </a> |
      <a href="https://github.com/felixrieseberg/windows95/releases/download/v5.0.1/windows95_5.0.1_armhf.deb">
        💿 deb
      </a><br />
    </td>
  </tr>
</table>

<hr />

<table width="100%">
  <tr>
    <td width="50%">
      <img src="https://github.com/user-attachments/assets/43ab7126-765e-444b-ad14-27b1beadbc7c" width="100%" alt="Screenshot showing Windows 95">
    </td>
    <td width="50%">
      <img src="https://github.com/user-attachments/assets/7ac5dc36-cbd4-4455-a616-0e5cca314b34" width="100%" alt="Screenshot showing Windows 95">
    </td>
  </tr>
</table>

## Does it work?
Yes! Quite well, actually - on macOS, Windows, and Linux. Bear in mind that this is written entirely in JavaScript, so please adjust your expectations.

## Should this have been a native app?
Absolutely.

## Does it run Doom (or my other favorite game)?
You'll likely be better off with an actual virtualization app, but the short answer is yes. In fact, a few games are already preinstalled - and more can be found on the Internet, for instance at [archive.org](https://www.archive.org). [Thanks to
@DisplacedGamers](https://youtu.be/xDXqmdFxofM) I can recommend that you switch to a resolution of
640x480 @ 256 colors before starting DOS games - just like in the good ol' days.

## Credits

99% of the work was done over at [v86](https://github.com/copy/v86/) by Copy aka Fabian Hemmer and his contributors.

## Contributing

Before you can run this from source, you'll need the disk image. It's not part of the
repository, but you can grab it using the `Show Disk Image` button from the packaged
release, which does include the disk image. You can find that button in the
`Modify C: Drive` section.

Unpack the `images` folder into the `src` folder, creating this layout:

```
- /images/windows95.img
- /images/default-state.bin
- /assets/...
- /bios/...
- /docs/...
```

Once you've done so, run `npm install` and `npm start` to run your local build.

If you want to tinker with the image or make a new one, check out the [QEMU docs](./docs/qemu.md).

## Other Questions

 * [MS-DOS seems to brick the screen](./HELP.md#ms-dos-seems-to-brick-the-screen)
 * [Windows 95 is stuck in a bad state](./HELP.md#windows-95-is-stuck-in-a-bad-state)
 * [I want to install additional apps or games](./HELP.md#i-want-to-install-additional-apps-or-games)
 * [Running in Docker](./docs/docker-instructions.md)
 * [Running in an online VM with Kubernetes and Gitpod](./docs/docker-kubernetes-gitpod.md)

## License

This project is provided for educational purposes only. It is not affiliated with and has
not been approved by Microsoft.

---

### Overview (`guest-tools/README.md`)

# guest-tools

Files and folders in this directory are exposed read-only inside the VM at
`\\HOST\TOOLS` (alongside the synthetic `_MAPZ.BAT` and `README.TXT`).
Drop drivers and utilities here that you want available from within
Windows 95.

## mouse-driver/ — seamless mouse (VBADOS)

`VBMOUSE.EXE` (DOS TSR) + `VBMOUSE.DRV` (Windows 3.x/9x driver) from
[VBADOS](https://git.javispedro.com/cgit/vbados.git/) by Javier S. Pedro,
GPLv2. Talks to v86's VMware mouse backdoor (port 0x5658) so the Windows
95 cursor tracks the host cursor pixel-for-pixel without pointer lock.

Install inside the guest:

1. Copy `\\HOST\TOOLS\mouse-driver\VBMOUSE.EXE` to `C:\` and add a
   `C:\VBMOUSE.EXE` line to `C:\AUTOEXEC.BAT`.
2. Windows Setup (or Control Panel → Mouse → General → Change → Have
   Disk) → browse to `\\HOST\TOOLS\mouse-driver` → pick **VBMouse int33
   absolute mouse driver**.
3. Reboot. The app detects the driver and stops grabbing pointer lock;
   ESC still toggles lock for games that want raw relative input.

## agent/ — W95TOOLS guest agent

`W95TOOLS.EXE` is a hidden-window agent that talks to the emulator over
the VMware backdoor (port 0x5658). Currently it:

- bridges Windows 95's `CF_TEXT` clipboard to the host (legacy backdoor
  commands 6–9; host side is `src/renderer/clipboard.ts`, which polls
  Electron's clipboard),
- auto-maps `\\HOST\HOST` to `Z:` at login via `WNetAddConnection`, so
  the shared folder shows up as a drive without a trip through
  Start → Run,
- keeps the Windows clock set to the host's local time (backdoor
  GETTIME, command 23). Windows only reads the RTC at boot, so without
  this a resumed emulator session carries on with the clock from
  whenever the state was saved. The agent checks every 5 seconds and
  steps the clock whenever it's off by 2 seconds or more — covering
  resume, slow-emulation drift, and the UTC-only RTC at boot.

Host-initiated shutdown and a tray icon will live here too when those
land.

Install inside the guest:

1. Copy `\\HOST\TOOLS\agent\W95TOOLS.EXE` to `C:\WINDOWS\`.
2. Drop a shortcut to it in
   `C:\WINDOWS\Start Menu\Programs\StartUp` so it runs on login.

Copy text on either side and it appears on the other within ~250 ms.
Text only; conversion is Windows-1252 ↔ UTF-8 with CRLF ↔ LF, capped at
64 KB. Built from `w95tools.c` with Open Watcom v2 — `make -C
guest-tools/agent` (needs Docker).

To update an already-installed agent: end the running `W95tools` task
(Ctrl+Alt+Del → End Task) or reboot Windows, copy the new
`\\HOST\TOOLS\agent\W95TOOLS.EXE` over `C:\WINDOWS\W95TOOLS.EXE`, and
start it again (the StartUp shortcut also picks it up at next login).

### Baking into the disk image

Fresh boots run whatever is baked into `images/windows95.img` — the
agent is installed there twice: at `C:\WINDOWS\W95TOOLS.EXE` and as a
copy in `C:\WINDOWS\Start Menu\Programs\StartUp`. You can bake a new
build directly from the host with mtools (`brew install mtools`), no
QEMU boot needed. The FAT32 partition starts at sector 63 (byte offset
32256):

```sh
make -C guest-tools/agent
IMG="images/windows95.img@@32256"
mcopy -o -i "$IMG" guest-tools/agent/W95TOOLS.EXE '::/WINDOWS/W95TOOLS.EXE'
mcopy -o -i "$IMG" guest-tools/agent/W95TOOLS.EXE \
  '::/WINDOWS/STARTM~1/PROGRAMS/STARTUP/W95TOOLS.EXE'
```

Do this while the app is not running. Note that an existing saved
state (`state-v4.bin`+) keeps its own dirty-block overlay over the
image *and* the old agent stays loaded in the resumed session's RAM —
so resumed sessions only pick up a new agent after a guest reboot or
an End Task + relaunch.

---

### Overview (`src/renderer/smb/README.md`)

# SMB1 server for Windows 95

Zero-dependency SMB1/CIFS server that lets Windows 95 (running inside v86) mount
a host folder as a network drive. Read-only. ~1500 lines.

## Stack

| Layer | File | What it does |
|---|---|---|
| Ethernet/IP/UDP | `nbns.ts` | Taps `bus.register("net0-send")` for raw frames, parses UDP 137, builds reply frames manually |
| NetBIOS Name Service | `nbns.ts` | Answers Node Status (0x21) and Name Query (0x20) — Win95 won't try TCP until this resolves |
| TCP 139 hook | `index.ts` | Monkeypatches `adapter.on_tcp_connection` (old v86) or registers `tcp-connection` bus event (new v86) |
| NetBIOS Session | `netbios.ts` | RFC 1002 framing — 4-byte header, reassembles fragmented TCP |
| SMB1 wire | `wire.ts`, `smb.ts` | Little-endian Reader/Writer, header parse/build |
| Commands | `server.ts` | NEGOTIATE, SESSION_SETUP, TREE_CONNECT, TRANSACTION (RAP), TRANSACTION2, SEARCH, OPEN, READ, CLOSE, etc. |

## Protocol gotchas (learned the hard way)

### NEGOTIATE: NT LM 0.12 is the only path to long filenames
Win95 offers `["PC NETWORK PROGRAM 1.0", "MICROSOFT NETWORKS 3.0", "DOS LM1.2X002",
"DOS LANMAN2.1", "Windows for Workgroups 3.1a", "NT LM 0.12"]`. We pick
`NT LM 0.12` and send the 17-word NT response (Capabilities=0 — no UNICODE, no
NT_STATUS, no NT_FIND, so the rest of the protocol stays OEM/DOS-error). On any
LANMAN dialect Win95's redirector lists directories via `CMD_SEARCH` (0x81) whose
13-byte name field hard-caps at 8.3; under NT LM 0.12 it switches to
`TRANS2/FIND_FIRST2` and asks for level `0x104` (FILE_BOTH_DIRECTORY_INFO)
**regardless** of CAP_NT_FIND. We implement that level — the 94-byte fixed prefix
plus OEM long name, ShortName always UTF-16LE per spec. The 13-word LANMAN
response is kept as a fallback for clients that don't offer NT.

### Shares
Two disk shares plus IPC$. The user share is named after `path.basename()` of the
mounted folder (sanitized, ≤12 chars). `TOOLS` is purely synthetic — `_MAPZ.BAT`,
`README.TXT` — so the user's listing isn't cluttered. `treeConnect` routes by
share name to a TID; every path-resolving handler branches on TID so the TOOLS
tree never touches the host fs.

The user share's backing directory is re-validated on every TREE_CONNECT and
NetShareEnum (it can be deleted — or restored — at any time after the path was
saved in settings). While it's missing, connects to that share return
ERRSRV/ERRinvnetname ("network name cannot be found"), the share is hidden from
enumeration, and TOOLS/IPC$ keep working. `SmbSession` accepts a null root for
the "no folder configured" case and must never throw from its constructor — see
the bus-listener note below.

### SEARCH (0x81): single-file probes vs wildcard listings
`SEARCH "\FOO.TXT"` is a stat probe — Win95 wants exactly one entry back. If you
prepend `.` and `..` like you would for `\*`, Win95 reads the first entry (`.`,
attr=DIRECTORY) and treats `FOO.TXT` as a folder. Only prepend dots when the
pattern contains `*` or `?`.

### SEARCH filename: null-terminate before padding
The 13-byte name field must be `name\0\0\0...`, not `name   \0`. Space-padding
before the null means Win95 sees `FOO.BAT   ` (with trailing spaces) and can't
match the `.BAT` file association.

### 8.3 mapping needs `~N` suffixes, not just truncation
84 files in a real Downloads folder → most have long names → naive truncation
gives 30 copies of `15_UNDER.PDF`. Use Windows-style `~N` and keep a per-dir
SFN→real-name map so OPEN can find the actual file. `resolve()` walks each path
component through the map.

### RAP (TRANSACTION 0x25): Win95 loops until ServerGetInfo answers
After `TREE_CONNECT \\HOST\IPC$`, Win95 sends RAP NetShareEnum (func=0, `WrLeh`/
`B13BWz`) then NetWkstaGetInfo (func=63, `WrLh`/`zzzBBzz`) then NetServerGetInfo
(func=13, `WrLh`/`B16BBDz`). The data descriptor tells you the layout:
`B16` = 16-byte inline name, `z` = string pointer (4 bytes into a heap that
follows the struct), `B` = byte, `D` = dword. We synthesize the struct from the
descriptor so any info-level Win95 asks for gets a plausible reply.

### Virtual files need to be visible to QUERY_INFORMATION too
The injected `_MAPZ.BAT` showed in listings but Win95 stats before opening,
got ERR_BADFILE, said "cannot find". Hook `getVirtual()` into QUERY_INFO and
CHECK_DIRECTORY, not just OPEN.

## v86 integration (the hard part)

### Old v86 (Feb 2025 — what currently boots): connection theft
The `tcp-connection` bus event was added later. The old API is
`adapter.on_tcp_connection(packet, tuple)` — you must construct `TCPConnection`
yourself, but it's closure-scoped in Closure-compiled `libv86.js`. Worse,
`.on()`/`.emit()`/`events_handlers` were dead-code-eliminated; the data callback
is a flat `.on_data` property.

The trick: shadow `adapter.receive` with a no-op (own-prop on a prototype method
— **must** restore via `delete`, not reassignment), call the original handler
with a fake port-80 SYN, take the `TCPConnection` it builds, re-aim it at port
139. `accept(packet)` overwrites all routing fields (sport/dport/hsrc/psrc/seq/
ack), `.on_data = handler` replaces the HTTP callback.

### New v86: just `bus.register("tcp-connection")`
Clean API. The new code keeps both paths; the bus event is a no-op on old builds.

### Exception in a bus listener kills the emulator
`bus.send` doesn't catch listener exceptions. They bubble through ne2k →
`port_write8` → wasm and unwind `do_tick()` before it can schedule
`next_tick()` — the whole VM freezes (instruction counter stops, CPU not
halted), not just SMB. The corrupted state then gets saved by
`onbeforeunload`. Wrap everything that runs in a callback:

- the `tcp-connection` bus handler and the old-API `on_tcp_connection` hook
  (`index.ts`) are wrapped in try/catch — any failure declines the connection,
- the per-connection data handler (`index.ts`) drops the frame instead,
- the NBNS `net0-send` handler (`nbns.ts`) drops the frame instead,
- `SmbSession`'s constructor never throws (a stale share root degrades to
  per-tree SMB errors; see "Shares" above).

This was a real bug: a share path deleted after being saved made
`new SmbSession` throw ENOENT inside the tick on the guest's first port-139
SYN, freezing the entire VM seconds after every boot.

## Security
- Read-only.
- Path traversal blocked lexically (`../`) AND through symlinks: `realpathSync`
  the deepest existing ancestor, re-append the unresolved tail, confirm under
  root. Symlinks pointing inside the share still work; symlinks pointing out
  return ERR_BADFILE.
- Share path validated in main-process IPC (`realpathSync` + `isDirectory()`),
  re-checked at emulator startup (`emulator.tsx`), and re-resolved on every
  TREE_CONNECT — a directory deleted after being saved degrades to SMB errors
  instead of crashing.

## Tests
`test-standalone.ts` — 76 protocol tests, full round-trips with real file I/O.
Run: `npx ts-node --skip-project --transpile-only --compiler-options
'{"module":"commonjs","moduleResolution":"bundler","ignoreDeprecations":"6.0"}'
src/renderer/smb/test-standalone.ts`

---

### Docker Instructions (`docs/docker-instructions.md`)

# Running windows95 in Docker

## Display using a volume mount of the host X11 Unix Socket (Linux Only):

**Requirements:**
* Linux OS with a running X-Server Display
* [Docker](http://docker.io) 

        docker run -it -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$DISPLAY --device /dev/snd --name windows95 toolboc/windows95


Note: You may need to run `xhost +` on your system to allow connections to the X server running on the host.

## Display using Xming X11 Server over tcp Socket (Windows and beyond):

**Requirements:**
* [Xming](https://sourceforge.net/projects/xming/)
* [Docker](http://docker.io) 

1. Start the Xming X11 Server
2. Run the command below:

        docker run -e DISPLAY=host.docker.internal:0 --name windows95 toolboc/windows95

## Display using the host XQuartz Server (MacOS Only):
**Requirements:**
* [XQuartz](https://www.xquartz.org/)
* [Docker](http://docker.io) 

1. Start XQuartz, go to `Preferences` -> `Security`, and check the box `Allow connections from network clients`
2. Restart XQuartz
3. In the terminal, run 
```
xhost +
```
4. run 
```
docker run -it -e DISPLAY=host.docker.internal:0 toolboc/windows95
```

---

### Docker Kubernetes Gitpod (`docs/docker-kubernetes-gitpod.md`)

## Running an online version of windows95
You can also run windows95 in Electron, in a virtual X server, in a JavaScript VNC client, in a Kubernetes workspace. What could go wrong?

[](https://gitpod.io/#https://github.com/felixrieseberg/windows95)

---

### Qemu (`docs/qemu.md`)

# QEMU Instructions

The image built here was made with QEMU. In this doc,  I'm keeping instructions
around.

Disk image creation

```sh
qemu-img create -f raw windows95_v4.raw 1G
```

ISO CD image creation

```sh
hdiutil makehybrid -o output.iso /path/to/folder -iso -joliet
```

Installation
```sh
qemu-system-i386 \
    -cdrom Win95_OSR25.iso \
    -m 128 \
    -hda windows95.img \
    -device sb16 \
    -nic user,model=ne2k_pci \
    -fda Win95_boot.img \
    -boot a \
    -M pc,acpi=off \
    -cpu pentium
```

- Boot from floppy
- Run `fdisk` and `format c:`
- Run `D:\setup.exe` with `24796-OEM-0014736-66386`
- After completing setup and restarting your computer, you might get an IOS Windows protection error
- Use `fix95cpu.ima` as a bootable floppy to fix
- Use `vga-driver.iso` to install different video driver

```sh
qemu-system-i386 \
    -m 128 \
    -hda images/windows95.img \
    -device sb16 \
    -M pc,acpi=off \
    -cpu pentium \
    -netdev user,id=mynet0 \
    -device ne2k_isa,netdev=mynet0,irq=10
```

## Mouse: keep `vmport=off`

The image has VBADOS (`VBMOUSE.EXE` + `VBMOUSE.DRV`) installed for seamless
host-cursor tracking in the app. Don't enable QEMU's VMware backdoor
(`-M pc,vmport=on`) expecting the same thing — the cursor becomes unusably
laggy. The `yarn run qemu` script therefore passes `vmport=off`, which makes
VBMOUSE fall back to a plain relative PS/2 mouse (click the window to grab,
Ctrl+Alt+G to release).

Why it breaks with `vmport=on`: QEMU's `vmmouse` queues **every** host
pointer event (4 words each, up to 256 events) and notifies the guest by
injecting a fake PS/2 packet per event. VBMOUSE reads exactly **one**
4-word packet per PS/2 interrupt (`mousetsr.c, handle_ps2_packet` — an
`if`, not a `while`). Whenever a single notification is dropped (PS/2
disabled during driver init, PS/2 output queue full, boot), the queue gains
a permanent backlog: from then on the guest only ever reads stale events
and the cursor trails minutes behind. v86 doesn't have this problem because
our `vmware-abspointer` patch coalesces motion packets in place — the guest
is never more than one move behind by design.

Fixing it for real would mean either teaching QEMU's `hw/i386/vmmouse.c` to
coalesce motion events, or patching VBADOS to drain the whole queue per
interrupt and baking the rebuilt driver into the image.

## Slimming the image before a release

After working in the image (installing things, building, browsing), two
kinds of dead weight accumulate:

1. **Deleted-file remnants.** FAT deletion only marks clusters free — the
   old bytes stay on disk and bloat the compressed zip badly (at one point
   ~200 MB of the packed zip was deleted-file garbage).
2. **Content that has no function in this app**: 1996/97 online-service
   installers (AOL, CompuServe, MSN), setup cabinets, uninstall backups,
   NetMeeting, browser caches. (Help files are dead weight too, but they
   stay — F1 showing period-correct 1995 help is part of the charm.)

**The recommended workflow is to do all of this inside Windows** (in a
QEMU session): delete the dead content in Explorer, empty the Recycle Bin,
run ScanDisk, shut down cleanly. Win95's own filesystem operations keep
the disk in a state that v86 cold-boots. Then:

```sh
tools/probe-boot.sh            # cold-boot in the APP (v86) — required
yarn run qemu                  # optional second opinion
# regenerate images/default-state.bin from a successful cold boot
# bump STATE_VERSION in src/constants.ts
tools/pack-disk.sh             # build the new images zip
```

### Uploading the packed image

CI does not use `images/` from this repo. On tag builds it downloads the
zip from a GitHub release on a separate private repo and unzips it into
`images/` (see `tools/download-disk.sh` / `.ps1` and the "Download disk
image" steps in `.github/workflows/build.yml`). The wiring lives on
*this* repo:

- `vars.DISK_REPO` — the private images repo
  (currently `felixrieseberg/windows95-images`)
- `vars.DISK_TAG` — the release tag CI pulls (currently `v5`)
- `secrets.IMAGES_REPO_TOKEN` — token with read access to `DISK_REPO`

To ship a new image:

```sh
tools/pack-disk.sh images_v6.zip                   # pack from images/
gh release create v6 -R felixrieseberg/windows95-images \
  --title "v6 disk image" images_v6.zip            # upload
gh variable set DISK_TAG --body v6                 # point CI at it
```

Constraints imposed by the download script:

- **Exactly one `.zip` asset per release** — it downloads with
  `-p '*.zip' -O images.zip` and errors if several assets match.
- **The zip must be flat**: `windows95.img` and `default-state.bin` at
  the archive root, no containing directory (`pack-disk.sh` gets this
  right; zipping the `images/` folder in Finder does not).

Existing releases/tags on the images repo are immutable history — make a
new tag for a new image rather than replacing assets on an old one, so a
re-run of an old build still gets the bytes it shipped with.

**Verify in the app, not just QEMU, and expect flakes.** Cold boot in v86
currently fails sporadically on any image, and certain disk states fail
deterministically. For how to interpret probe verdicts (when to retry,
when to ship, when to declare the image broken), follow the policy in
`.claude/skills/probe-win95/SKILL.md` ("VXDLINK: flake vs. real bug").

### Offline modification (currently not recommended)

The image can also be modified offline with mtools (see the inspect-disk
skill), and `tools/zero-free-clusters.py` can zero free clusters without
touching the FAT. The results pass fsck and boot in QEMU — but offline
modification measurably worsens v86 cold-boot reliability on images that
have been through recent QEMU sessions, again due to the bug above. Until
that bug is fixed in the v86 fork, prefer the in-Windows workflow and ship
the image untouched. (Never zero free space with the classic "`mcopy` a
giant zero file, then delete it" trick — that one breaks v86 cold boot
deterministically.)

After any image content change you MUST regenerate
`images/default-state.bin` (the old saved state has the old FAT cached in
guest RAM) and bump `STATE_VERSION` in `src/constants.ts`.

---

### Smb Share (`docs/smb-share.md`)

# Host folder over SMB

Windows 95 can mount a host folder as a network drive. The server lives in
`src/renderer/smb/` — ~1500 lines, zero dependencies, read-only. Defaults to
`~/Downloads`, configurable in Settings.

## Inside Win95

Open **My Computer** → **Z:**. The `W95TOOLS.EXE` guest agent auto-maps
`\\HOST\HOST` to `Z:` at login, so the share is available as a drive letter
out of the box. If the agent isn't running, the underlying UNC path still
works via Start → Run → `\\HOST\HOST`.

NetBIOS over TCP/IP must be enabled (Control Panel → Network → TCP/IP
properties → NetBIOS tab). This is baked into the default state image.

## Architecture

One SMB session per `TCPConnection`, hooked off v86's network adapter. The
server speaks SMB1 (LANMAN2.1 dialect) because that's what Win95 negotiates.
Full breakdown in `src/renderer/smb/README.md` — that file has the protocol
gotchas learned during implementation (NT dialect trap, NetBIOS name
null-termination, 8.3 `~N` mapping, RAP descriptor parsing).

**Security:** read-only, symlink-aware path traversal guard, share path
validated in main-process IPC. Not exposed until `smbSharePath` is set in
settings or `WIN95_SMB_SHARE=...` is in the env.

## Tests

```sh
npx tsc --ignoreConfig --module commonjs --target es2020 --esModuleInterop \
  --moduleResolution bundler --outDir /tmp/smb-test --skipLibCheck \
  src/renderer/smb/*.ts && node /tmp/smb-test/test-standalone.js
```

35 protocol tests, full round-trips with real file I/O. No Electron needed.

## What's not implemented

- Writes (read-only by design, but OPEN is easy to extend)
- Long filenames via TRANS2 (we serve 8.3 through the legacy SEARCH path,
  which is enough for Win95 Explorer but loses the original casing/length)
- Multiple shares — everything is one share named `HOST`
- Authentication — guest access only

---

### V86 Patches (`docs/v86-patches.md`)

# v86 patches carried by windows95

windows95 builds v86 from a fork branch rather than upstream master.
Each fix lives on its own feature branch in `felixrieseberg/v86`, has an
upstream PR against `copy/v86`, and is merged into the integration
branch `felixrieseberg/v86:windows95-base` (which is what
`tools/update-v86.js` builds from). When a PR is merged upstream, drop
its row here, delete the feature branch, and rebase `windows95-base`.

| Fix | Branch | Upstream PR | windows95-base | Why we need it |
|---|---|---|---|---|
| Node fs loader in Electron renderer | [`electron-renderer-fs-loader`](https://github.com/felixrieseberg/v86/tree/electron-renderer-fs-loader) | [copy/v86#1540](https://github.com/copy/v86/pull/1540) | ✅ | `await import("node:fs/promises")` fails in an Electron renderer; disk images don't load. |
| IDE shared Command Block registers | [`ide-shared-registers`](https://github.com/felixrieseberg/v86/tree/ide-shared-registers) | [copy/v86#1541](https://github.com/copy/v86/pull/1541) | ✅ | Win9x hangs at splash on disks >535 MiB because per-device register writes violate the ATA shared-register-file spec. |
| VMware absolute-pointer backdoor | [`vmware-abspointer`](https://github.com/felixrieseberg/v86/tree/vmware-abspointer) | [copy/v86#1542](https://github.com/copy/v86/pull/1542) | ✅ | Port 0x5658 GETVERSION + ABSPOINTER_* so VBMOUSE can track the host cursor 1:1 without pointer lock. |
| VMware text-clipboard backdoor | [`vmware-clipboard`](https://github.com/felixrieseberg/v86/tree/vmware-clipboard) | — *(stacked on #1542)* | ✅ | Legacy backdoor commands 6–9 so `W95TOOLS.EXE` can sync `CF_TEXT` with the host. |
| VMware GETTIME backdoor | [`vmware-gettime`](https://github.com/felixrieseberg/v86/tree/vmware-gettime) | — *(stacked on the clipboard branch)* | ✅ | Win95 only reads the RTC at boot, so a resumed state keeps a stale clock; `W95TOOLS.EXE` polls GETTIME (command 23) and steps the clock back to host time. |
| Defer V86-mode VBE disable | [`vga-defer-vbe-disable-v86`](https://github.com/felixrieseberg/v86/tree/vga-defer-vbe-disable-v86) | [copy/v86#1543](https://github.com/copy/v86/pull/1543) | ✅ | Opening a windowed DOS box leaks vgabios's VBE-disable past Win9x's VDD and turns the screen to planar garbage. |
| fake_network: copy TCP addrs | [`fake-network-copy-tcp-addrs`](https://github.com/felixrieseberg/v86/tree/fake-network-copy-tcp-addrs) | — | ✅ | `TCPConnection` routing fields alias the NE2000 TX ring; concurrent guest traffic retargets async replies and the guest RSTs them. |

## Adding a fix

1. Branch off `origin/master` in `../v86`, commit, push to `fork`.
2. Open the PR against `copy/v86`.
3. `git checkout windows95-base && git merge --no-ff <branch> && git push fork windows95-base`
4. `node tools/update-v86.js` in this repo to rebuild `libv86.js` / `v86.wasm`.
5. Add a row above and a bullet in `.claude/skills/update-v86/SKILL.md`.

---

--- METRICS ---
- Files Extracted: 8
- Estimated Token Budget: ~7223 tokens
- Canonical Reference: https://codewiki.google/github.com/felixrieseberg/windows95
