Back to News & Insights
Artificial Intelligence October 3, 2026 · 13 min read

Claude Code mods: Minesweeper and testkit

My own harness, the tooling I run around Claude Code, comes with a custom trust score. That trust...

Claude Code mods: Minesweeper and testkit

My own harness, the tooling I run around Claude Code, comes with a custom trust score. That trust score decides whether I can leave the agent alone yet, without supervision or if it's in my best interest to babysit it. As you see in the header image, it's babysitting era, so I built a Minesweeper to kill time between turns.

From Claude Code 2.1.287, mods load by default. A mod is a plugin whose hooks are JavaScript or TypeScript functions running inside Claude Code instead of shell commands, and it can open and draw its own panes. Minefield is Minesweeper as a mod: /mines opens a pane beside the transcript, and you clear the board while Claude works.

It is one 9×9 board with 10 mines, a game for the few minutes Claude works. The first reveal is always safe and opens an area, clicking a fully flagged number opens the cells around it, and the best time is kept between sessions. The orange face follows your cursor. Stop for six seconds and it pretends to read the transcript beside it, eyes half shut, line by line. It only looks; the mod sees nothing of the transcript. It winks at flags and puts on sunglasses when you win. This was the most fun part to implement.

It ships with 31 tests that run through claude plugin test, and CI runs them on two pinned Claude Code builds. Four of its bugs passed the test kit anyway and only showed up in a real session: the pane couldn't take the keyboard, the hotkeys did nothing, the face wrapped, and the mouse wasn't there.

In claude plugin test, nearly every engine call your mod makes (ui.open, ui.panes, store.get) is answered by an on(...) stub you write in the test file; the kit answers a few itself, such as $.ui.invalidate. That is why it runs fast and without credentials. It also means the host in your tests behaves exactly as well as your doubles do.

Seen in live sessions on 2.1.285: a pane takes the keyboard only over an empty prompt. Anthropic's docs list focus: true from a command as one way a pane gets the keyboard, but while command.run runs, the command's own text is still in the prompt, and on 2.1.285 the request was refused. The pane opens and the keyboard stays where it was.

The fix is to open the pane, then ask for the keyboard again once /mines has returned. register.ts waits 250 ms, then 500, then 1000 between tries (FOCUSRETRIESMS), three tries inside two seconds, and every retry first asks $.ui.panes() whether the pane is still open and still without the keyboard. If the pane is gone, the person closed it, and a retry must not bring it back.

What no test here can check is the refusal itself. isFocused in that test is whatever my double says it is.

A pane has two places to draw from. The hooks module (register.ts) answers the ui.render hook and returns elements: boxes, text, buttons. A Client is a surface module (board.ts) with its own state, a frame timer, and pointer and key handlers, which is where a game wants to live. So the control buttons went in the board, next to the cells they act on, each with a one-letter hotkey.

None of the hotkeys fired. A Button's hotkey fires only for buttons the hooks module draws, never for buttons a Client draws.

The tests had no way to notice. ui.press({ key: 'flag' }) presses a button by its element key ('flag', where the hotkey is f), so a press in a test never goes through the hotkey at all.

The fix moved the buttons into register.ts. That raised the next problem: the buttons now live in the hooks module, the game lives in the board, and the board learns about presses only through its props. Several presses can arrive between two redraws of the board, so handing over only the latest move drops the others. Each press gets a number instead, and the hooks module keeps the last 32:

A mounted test can't catch the race this guards against. The kit redraws after every press, so it never coalesces presses, and a mounted test of the queue passes even with the bug in place. So the race is tested as a pure function, with the presses that land in one redraw written out by hand:

The face is four rows of text, nine columns wide (FACEWIDTH = 9), drawn in Claude Code's own orange: the theme's claude colour, so it follows your theme. The status sits beside it in a row of boxes.

A row of boxes shrinks every box to fit. In a real pane the face's column came out one column short, and each of its nine-column rows wrapped in two. Eight rows of orange, and a face that looked like something had sat on it.

The tests find the face by its text (eyes(' ● ● '), a ui.find for that Text in the board), and the text was right. The strings were nine columns wide; the column holding them was eight.

The fix is a fixed width plus flexShrink: 0 on the face's column. The status column beside it shrinks and truncates (wrap: 'truncate-end'), so the long status is the part that gets cut, and the face's own rows truncate rather than wrap:

On Claude Code's main screen, the pane gets no clicks. Claude Code asks the terminal for pointer reporting only in the fullscreen layout: CLAUDECODENOFLICKER=1 at start, or /tui fullscreen in a session. On the main screen it asks for none at all.

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