Back to News & Insights
Artificial Intelligence October 5, 2026 · 9 min read

Claude Code mods: but does it run Doom?

Yes ... Yes it does. The Minesweeper was the warm-up. A pane that can draw pixels and take the...

Claude Code mods: but does it run Doom?

The Minesweeper was the warm-up. A pane that can draw pixels and take the keyboard raises the only question the internet ever asks of a new screen, so the second game in the arcade is Doom: Freedoom on the doomgeneric engine, in a pane beside the transcript, while Claude works.

/doom opens the pane and starts the game. In kitty the screen is a real picture at Doom's own 320×200 (Ghostty speaks the same protocol, but I haven't tried it); in any other terminal it is drawn in quadrant block characters, four pixels a cell. Walk with w and s, turn with a and d or by dragging sideways on the game, fire with space or the right mouse button.

Getting a picture on screen took an afternoon. The controls took the rest of the week, because a terminal never tells you that a key was let go.

A mod's hooks run in a sandbox: no Node, no DOM, no require, and deliberately no WebAssembly. The typings say what to do instead: run the heavy thing as a program of its own. So the game is doomgeneric compiled to a native binary, and the mod starts it with $.process.spawn:

A spawned child lives as long as the mod reads its output and ends when the mod unloads, so there is no daemon to clean up. The engine prints doom-claude listening once it is ready, and from then on the mod talks to it with $.http.fetch: over a Unix socket on Linux and macOS (socketPath), and over 127.0.0.1 with a random 64-digit token on Windows, where every request without the token gets a 403.

There is no compiler on the player's machine to count on, so the arcade ships six prebuilt engines, cross-compiled with zig cc from one Linux box: Linux, macOS and Windows, each on x8664 and arm64. Honest status: only the Linux x8664 one has run. The macOS and Windows builds are compiled and checked (file says Mach-O and PE32+, the arm64 Mach-O carries its ad-hoc signature), and nobody has started one yet.

In kitty and Ghostty, Claude Code's Image element can take a file instead of bytes. The engine writes each frame as raw RGB to a file beside its socket (written aside, then renamed over the old one, so it is never read half-written), and the mod points the Image at it:

Claude Code hands kitty the path (a=T,…,t=f in the graphics protocol), kitty reads the pixels itself, and a new generation makes it read the file again. Not one pixel goes through Claude Code.

Every other terminal gets a Raster, and a Raster has opinions. It rounds every colour to 4 bits a channel. It paints at most 1024 colour pairs and snaps the rest, which speckles a photo-like picture. And it leaves blank cells at the end of a row undrawn, so a dark corridor came out with black holes in it. The engine pre-encodes each frame into cells: a 32-colour palette per scene (32 × 32 is 1024), each cell the quadrant glyph and the two colours that fit its four pixels best, a cell reusing its left neighbour's colours when they fit nearly as well, and no blank cells at all.

The first live run froze for 300 ms at a time. $.ui.blit repaints a Raster at Claude Code's next frame, and with nothing else on screen moving, Claude Code draws only about three frames a second. The game was sending 30.

The fix is a heartbeat: the red strip under the game is a Client, and it redraws itself every 30 ms, swapping one blank character for another. That keeps Claude Code drawing, and it costs far less than redrawing the whole pane from the hooks module. One more trap on the way: a blit resolves when it has been painted, not when it has been taken, so the frame loop must not await it before fetching the next frame.

On a machine with other work running (load 5–6 on 8 cores), the picture changed 35–40 times a second in a plain terminal and 31–32 times a second in kitty. The block picture costs Claude Code about a full core; the kitty picture about a third of one.

Doom wants to know when a key goes down and when it comes up. A terminal reports the press, then, after its repeat delay, the same press again every 30 ms or so, and never the release. Claude Code turns on kitty's keyboard protocol with flags 5 only, so a mod gets no release events and can't even tell a repeat from a new press. The terminal also repeats only the newest key: hold w, press a, and w goes quiet whether your finger is still on it or not.

So every terminal Doom fakes the release, and my first attempt was a guess. A turn key held for 220 ms, so a tap would be a small turn. Then the terminal's first repeat came at 500 ms, and a held turn key stopped dead for 308 ms before it started turning for real. I measured it by reading the player's facing out of the engine every 10 ms. Then I patched it, and patched the patch.

| port | fakes the release how | |---|---| | doom-cli | learns the terminal's repeat delay and rate per key; a fresh press holds until the first repeat is due, a repeat until the next is due | | intermission | 550 ms after a fresh press, 120 ms while repeating; turning is mouse-only | | claude-doom | a 160 ms pulse per press | | doom-braille | released after 100 ms with no bytes |

doom-cli's model is the one that holds up, so the engine now does the same. A movement key holds until its next repeat is due. The repeat delay is learnt from the gap between a press and its first repeat (the median of the last five, capped at 600 ms), so it fits GNOME's 500 ms as well as anyone's faster setting:

That has a cost doom-cli's own source admits in a TODO: a tap now lasts as long as the repeat delay, so one tap of d turned 61.5°. The TODO also names the fix: "just turn more slowly outside of state repeat? so it's still possible to do some precision aiming". So a turn key in play doesn't press Doom's arrow key at all. It turns through Doom's mouse, slowly until the terminal starts repeating it, then at the arrow keys' full speed, with the same half-speed first six tics Doom gives a held key:

A tap of d now turns 10.5°, about what a quick tap gives in Doom with a real keyboard, and a held d never stops for longer than 28 ms. In a menu, a pause or the title demo, the arrows stay arrow keys, because menu sliders want them.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation