windows95 Documentation

Windows 95 in an app. Runs on macOS, Linux, and Windows.

24,264 stars TypeScript TypeScript 1 file ยท ~7,308 tokens
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy windows95 on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name windows95 --restart always felixrieseberg/windows95:latest

Technical Documentation: felixrieseberg/windows95

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (`README.md`)

windows95

This is Windows 95, running in an Electron app. Yes, it's the full thing. I'm sorry.

Downloads

| Windows | 32-bit

text
๐Ÿ’ฟ Installer
    
  
    ๐Ÿ“ฆ Standalone Zip
  
  
  64-bit
  
    ๐Ÿ’ฟ Installer
    
  
    ๐Ÿ“ฆ Standalone Zip
  
  
    โ“ Don't know what kind of chip you have? It's probably `x64`. To confirm, on your computer, hit Start, enter "processor" for info. |

| --- | --- |
| macOS | Apple Silicon Processor

text
๐Ÿ“ฆ Standalone Zip
  
  Intel Processor
  
    ๐Ÿ“ฆ Standalone Zip
  
  
    โ“ Don't know what kind of chip you have? If you bought your computer after 2020, select "Apple Silicon". Learn more at [apple.com](https://support.apple.com/en-us/HT211814). |

| Linux | 64-bit

text
๐Ÿ’ฟ rpm
    
  
    ๐Ÿ’ฟ deb
  
  ARM64
  
    ๐Ÿ’ฟ rpm
    
  
    ๐Ÿ’ฟ deb
  
  ARM
  
    ๐Ÿ’ฟ rpm
    
  
    ๐Ÿ’ฟ deb |

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. Thanks to
@DisplacedGamers
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 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:

text
- /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.

Other Questions

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 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

    text
    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:

  1. Start the Xming X11 Server

  2. Run the command below:

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

Display using the host XQuartz Server (MacOS Only):

Requirements:

  1. Start XQuartz, go to Preferences -> Security, and check the box Allow connections from network clients
  2. Restart XQuartz
  3. In the terminal, run
text
xhost +
  1. run
text
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?


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").

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 copy/v86#1540 โœ… await import("node:fs/promises") fails in an Electron renderer; disk images don't load.
IDE shared Command Block registers ide-shared-registers copy/v86#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 copy/v86#1542 โœ… Port 0x5658 GETVERSION + ABSPOINTER_* so VBMOUSE can track the host cursor 1:1 without pointer lock.
VMware text-clipboard backdoor 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 โ€” (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 copy/v86#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 โ€” โœ… 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 && 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 ---