emu86
A 1980s PC written in TypeScript, booting a real Unix in your browser tab — a machine that keeps its own state, where duplicating the tab plugs a second machine into the network, and one tab can telnet into the other.
What this is
emu86 is an Intel 8086 PC emulator written in plain TypeScript — no WebAssembly, no native code, no runtime dependencies beyond the language itself. It boots unmodified ELKS (the Embeddable Linux Kernel Subset, a real Unix for 16-bit machines) from real disk images: BIOS, interrupt controller, timer, UART, keyboard, IDE disks, and an NE2000 network card, all modeled honestly enough that the stock kernel probes them and finds them.
It exists to answer a question for a different project: a browser code editor needed an 8086 emulator, and the choice was pure TypeScript versus C compiled to WASM. This is the pure-TypeScript arm of that bet — the wager being that readable source is worth more than raw speed, and that the speed would turn out to be enough. The project's stated principle from day one: understandable first, fast second. (The verdict, measured: on genuine kernel workloads it sustains about half of a 4.77 MHz cycle budget under the strictest accounting there is — one instruction per cycle, a standard no real 8086 ever met. A real XT spent four to twelve cycles per instruction, so in instructions actually delivered this is several XTs — which is why the machine ships with an authenticity governor, not a speed boost: uncapped, Tetris is unplayably fast. Speed was never the problem.)
The timeline
First life, inside the editor project itself: a 23-file emulator core validated bit-for-bit against Adrian Cable's 8086tiny, running both side by side in a C co-simulation harness and diffing every register after every instruction.
The standalone repository. Three days, seven commits: from a six-opcode proof-of-concept to CGA text, a keyboard, a serial console, and a browser harness. Then the repo went quiet — the work didn't. The following phases were driven from, among other things, a folding phone.
Phase by phase, brief by brief: interrupt delivery, the PIT and PIC, ELKS boot diagnosis (early printk, root mount), hard-disk images, MBR partitions, multi-disk, a MINIX-filesystem image with a complete on-disk C toolchain. Each phase ends with a written report that records what worked and what didn't — the abandoned approaches are documented as carefully as the shipped ones. Twenty-nine briefs, and the test count climbing the whole way.
Consolidation day: three machines' checkouts verified identical, folded into one, audited top to bottom. Then Phase 14 opened — the dogfooding arc — with a hello-world compiled and run inside the emulated machine, using the C compiler that lives on the guest's own disk, and the resulting binary extracted back out byte-exact, checksummed on both sides of reality.
The network grew up in one long arc: an NE2000 card the stock
kernel detects; a virtual ethernet switch; a gateway that answers
ARP and ping; and then the TAN — the Tab Area Network —
where every browser tab's machine joins one LAN over a
BroadcastChannel trunk, leases itself an IP, and
telnet 10.0.2.x logs you from one tab's Unix into
another's. Duplicating the tab became the fastest PC purchase in
history. DNS followed — with a surprise (see below) — then real
answers via DNS-over-HTTPS pass-through.
The site you're on went live. Boot scripts that type themselves at the right prompts. Sticky per-tab IP addresses. And two fixes a real 1981 machine would be proud of: a pacing overhaul so that guest time is wall time — one guest second is one real second, idle or busy, capped at an authentic 4.77 MHz with a Turbo switch — and an MC146818 real-time clock chip, after which the machine stopped believing it was October 21, 1991.
The machine grew a memory, and then it grew manners. A
block-level copy-on-write overlay made the boot disk durable —
what the guest writes, reloads keep, keyed to the base image by
SHA-256 so a wrong overlay is refused rather than folded. Every
tab got its own private /dev/hdb that survives with
it, an editor drawer over that drive (read and written through
the project's own MINIX filesystem module), and finally the
un-typed boot: a fresh machine formats its own home
drive, seeds it, logs itself in, and performs its first-boot
show — writing and compiling a C program with its own toolchain —
exactly once per drive, with nothing typed, ever. One long day of
field-testing later, the whole line was promoted; the previous
version is archived one click away, as every outgoing version now
is.
Things learned the hard way
- ELKS speaks DNS over TCP. Its network stack has no UDP at all — a fact apparently nowhere on the internet until you read the source. The plan said "DNS is easy, it's just UDP"; the guest disagreed, and a small TCP engine got built a milestone early.
- ELKS has no ping. The kernel answers pings
but nothing on the system can send one. The acceptance test
inverted itself: the LAN pings the machine, and the machine's own
netstatcounters testify. - A timeout that says "Name not found." The guest's resolver compares a signed error against an unsigned size, sails past its own error check, and parses its own transmit buffer as the answer. Two hours of wire-level debugging to learn the bug was upstream, in libc, since 2022.
sleep 30took one second. An idle emulated CPU burned virtual time thirty times faster than wall time; a busy one, three times slower. Games crawled while timeouts fired instantly. The fix — drive the virtual clock from the host's — is one page of code and was worth the whole milestone.- Telnet gaming is a fuzzer. Playing Tetris between two tabs found a ring-buffer wraparound bug in the NE2000 model that no test had touched. The crash reproduced, the seam got a regression test, the games resumed.
- The counterparty is the contract. Every device here is written against what the guest's source code actually does — ktcp's checksum quirks, the clock utility's weekday-plus-three convention — not against the datasheet. The datasheet doesn't boot.
cat /bootoptslies. The boot options are patched by a raw block write the kernel reads whole — but the file's inode still says 692 bytes, so any filesystem read shows only the pristine prefix. It had been quietly lying since the patch shipped; an hour of ghost-hunting later, the rule is written down: probe behavior, not files.- The buffer cache bites twice. A guest
rmthat hasn'tsynced is invisible to the host's snapshots — which replayed the first-boot show after a quick refresh, and separately stranded a home directory owned by the wrong user. Same seam, two costumes; the fix each time was a well-placedsyncand refusing to treat cached truth as disk truth.
How it's built
The source is on GitHub, MIT-licensed — the 59 phase reports and 34 briefs included, which means the abandoned approaches and wire-level debugging sagas described above are all readable in full.
This project is a long-running collaboration between one human and Claude (Anthropic's model — earlier phases planned by chat, current work done end-to-end in Claude Code sessions). The working discipline predates the tools being fashionable: every phase starts with a written brief agreed before implementation, and ends with a written report — findings, decisions, measurements, and explicitly what was not done. The reports are the project's memory; the code alone doesn't carry the reasoning. Nothing lands without the full test suite green, and the CPU core answers to an external oracle: the SingleStepTests 8088 corpus, thousands of single-instruction cases with exact expected machine state.
The hard rules have never moved: the CPU step stays purely synchronous, strict TypeScript stays strict, no new dependencies, and negative results are findings to publish, not failures to hide.
Try it
- Boot the machine — a first visit starts an ELKS floppy while quietly fetching itself a full hard disk (three megabytes over the wire; it unpacks to thirty-one), then stages the real machine for your next reload.
- Touch nothing. The machine formats its own home drive, logs
itself in as
user1, and — once per drive, the first time it meets you — types out a C program, compiles it with the toolchain on its own disk, runs it, and says hello. After that first show, every boot comes up with the network already on. - Duplicate the tab. The boot banner names the new machine
(
cat.tabs, say), and its drive is a fork of the original's. Thentelnet cat.tabsfrom the first tab — you are now remotely logged into a computer that lives one tab to the right. rma file. Reload. It's still gone — the machine keeps its own state now, per tab, and Settings → Machine state → Reset is the way back to factory. Set a password withpasswdand it will demand it forever after, which is both a feature and a warning.ping elk,nslookup example.com,urlget http://ipinfo.io/json— the gateway answers, real names resolve over DNS-over-HTTPS, and real HTTP flows through a kernel too old to imagine any of it.date— correct, thanks to a CMOS chip younger than the operating system reading it.- Playing games? Settings → CPU speed → Turbo. An authentic 4.77 MHz 8086 pushing full-screen redraws down a serial line is authentically slow; the Turbo button is period-correct.
- Slide out the
/mnt filesdrawer: a host-side editor over the tab's own drive. Edit a file, andresyncin the guest picks it up; edit in the guest, and the drawer follows along — telling you, never overwriting you, when the file you have open changed underneath.
Honest limits
- Outbound network magic is bounded by the browser: DNS works
(DoH is HTTPS), guest-to-guest TCP works (it's real ktcp on both
ends), and the guest's
urlgetreaches the real internet through a gateway that re-issues requests as browser fetches — which means CORS applies (API endpoints usually work; HTML pages usually don't, and the gateway now says so instead of muttering). HTTPS from the guest itself remains structurally impossible without tricks we've declined to play. - No inbound connections from the real internet. The TAN is same-origin, same browser.
- Under authentic pacing the engine sustains about half its one-instruction-per-cycle budget on heavy kernel work — still more delivered instructions than a real XT managed, but full-screen terminal redraws down a serial line can drag; that's what the Turbo switch is for.
- Persistence is honest, not miraculous: the machine's state
sweeps to storage on a heartbeat, so a tab killed mid-keystroke
can lose its last few seconds — the same class of loss as yanking
a floppy.
syncin the guest, then breathe.
Family
emu86 is the youngest of three siblings, and the family has a plot: all three are ways of putting a 1981 PC somewhere it has no business being.
-
retro.sophtwhere.com
— the eldest, and it predates everything here:
8086tiny itself, compiled to WebAssembly and running in a
browser worker, booting FreeDOS at a text terminal. (Its screen
is an xterm and nothing more — so while Windows 3.0 and the
cat game ride along on its disks, graphics software has no
porthole there; honesty about one's display is a family value.)
Built almost entirely by the same human's own hand (an AI
applied the final coat of paint). Which makes it,
literally, the other arm of the bet this project exists to
settle: that page is the C-compiled-to-WASM path; this page
is the pure-TypeScript path; the editor project that commissioned
both gets to compare them running. (It can boot ELKS too — its
demo is just too modest to admit it. And credit where due: both
the boot-script idea and the type-it-yourself demo on this site
are cribbed straight from it — though where the elder sibling
types into DOS
debugand cheats the date in at the prompt, this one types into a C compiler and grew a real CMOS clock chip instead. Same tricks, one generation of substrate-honesty apart.) - mcu-pc.net — a 1981 IBM PC emulated on a USB stick the size of a thumbnail, booting FreeDOS on a microcontroller with kilobytes to spare and playing the cat game on a screen the size of a fingernail. C on bare metal, against hardware limits; ninety-two commits in three days.
- emu86 (you are here) — TypeScript in a browser, against no limit but honesty. Four months, forty-one reports, and a network that exists between your tabs.
Same human throughout; different collaborators per era; one shared conviction — that the machines of 1981 are still worth understanding, and the proof is rebuilding them somewhere impossible.
And now there is a PC in your browser tab. It boots a real Unix and logs itself in, joins a network that exists only between your own tabs, resolves real names through a resolver its kernel is too old to imagine, keeps honest time from a clock chip it never shipped with, remembers what you did to it yesterday, and lets you telnet into its twin — a computer created, in full working order, by pressing "Duplicate Tab."