The short version
- Luminair's first Windows port landed on 21 July 2026. It built in CI, and it quietly carried a lot of Mac habits with it.
- On 9 September a Windows install opened with the macOS permissions sheet. The audit that followed found the screen had no synchronous way to know which OS it was on, and the main process called
/bin/zsh,psand other Mac tools without a guard. - The fixes: one platform switch before first paint, a shell and process list per OS, Ctrl in every label and chord, a Stop that kills the whole process tree, and a probe that runs the packaged engines on a real Windows runner.
- The installer then taught us the hardest lesson: it can say Completed with no app on disk. Every Windows release is now signed, silently installed and checked file by file before it ships.
Found by running it.
None of this came from a checklist. It came from installing the app on Windows machines and using it. The September fixes were written in Luminair sessions and carry a Claude co-author line in git: Claude Fable 5.1 for the audit and the shortcut work, Claude Opus 5.5 for the installer fixes. The audit notes were saved to the team's Journal the same evening, with a list of what was fixed and what was still open.
This post was drafted the same way: one session reading the repository, its history and those notes, with every outside claim checked against the page it links to.
The desk moved to Windows 11.
Microsoft's support page is blunt: “Windows 10 has reached the end of support on October 14, 2025.” After that date Microsoft no longer provides technical support, software updates, or “Security updates or fixes”, and it recommends moving to Windows 11.
That did not empty the world of Windows 10. A Slashdot summary of an Ars Technica report that day noted it “still runs on roughly 40 percent of the world's Windows PCs”, citing StatCounter, and that home users could buy one more year of Extended Security Updates. Tom's Hardware put it more simply: no more feature updates, no more official troubleshooting, “and most importantly, no more security updates.”
For a small team building a desktop agent app, the practical meaning is that the Windows machines people are setting up now are Windows 11 machines. The first installer incident later in this post happened on Windows 11. If you build developer tools on a Mac, a lot of your users are not on one.
“Let Luminair work.”
On a Mac, an agent app has to ask for things: microphone, camera, screen recording, files in protected folders. macOS keeps those behind its privacy system, and Luminair shows a first-run sheet that walks you through them, with buttons that open the right pane of System Settings through x-apple.systempreferences links.
On 9 September a Windows user got the same sheet. The buttons did nothing useful: Windows answered with “Get an app to open this link”, because no Windows program knows that address. The audit notes from that evening give the root cause in one line: the renderer had no platform awareness. Its only way to ask was one asynchronous call, and almost nothing made it.
The fix has three parts, all in the commit titled “Windows: stop showing macOS permissions and mac-only paths”:
Know the OS before first paint
The preload script now hands the renderer platformSync, a plain value with no round trip. Four constants hang off it: IS_MAC, IS_WIN, PRIMARY_MOD and MOD_GLYPH. The page also gets a data-os attribute so the stylesheet can hide Mac-only chrome.
Answer permission questions honestly
The permission status handler returns nothing off macOS, so the Mac sheet never appears. A day later the Settings page came back on Windows with only what Windows has: microphone, camera, location and Bluetooth, each opening its ms-settings: page.
Stop saying Mac
Copy that said “this Mac” now says “this computer”, and Finder becomes Explorer in the export help. Features that really are macOS-only, such as the live branch preview, now say so instead of failing when clicked.
Every spawn was a guess.
An agent app runs other programs all day: the engines themselves, git, installers for skills, health checks. On a Mac it is natural to reach for /bin/zsh and ps. On Windows neither exists, and a missing program does not always fail loudly.
The same-day follow-ups list what that looked like in practice. Background-job liveness enumerated ps, so on Windows every job “stayed alive until the hard ceiling”. The goal gate, which runs your verification command, spawned /bin/sh, “so every verification gate failed to start”. On machines without Git bash, skill installs sent a POSIX one-liner through cmd.exe and failed every time. The doctor's disk check used /bin/df. And every session on a Windows install was filed under Unsorted, because the folder check joined paths with a forward slash and Windows records C:\Users\me\proj\sub.
The replacements are small helpers with one job each. Shell one-liners go through posixShellFor, which prefers Git for Windows' bash if it is installed and falls back to cmd.exe:
function posixShellFor(cmd) { if (process.platform !== 'win32') return { file: process.env.SHELL || '/bin/zsh', args: ['-lc', cmd] }; // Program Files, Program Files (x86), LocalAppData: Git\bin\bash.exe for (const c of cands) if (fs.existsSync(c)) return { file: c, args: ['-lc', cmd] }; return { file: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', cmd] }; }
Process listings on Windows go through winProcessList, which asks PowerShell for Win32_Process and returns rows in the same shape the Mac code already parsed from ps. Folder containment goes through one path key that turns separators into slashes and lower-cases the path on Windows only, because Windows paths are case-blind and the drive letter can differ between the folder picker and the recorded working directory. Mac behaviour is unchanged and stays case-sensitive.
metaKey and never hardcode a ⌘ glyph, and the main-process helpers sit below the spawn guard, where a test checks that nothing reaches for child_process before the guard is installed.The Windows key closed the window.
Keyboard shortcuts looked like the easy part. Electron reports a meta flag for the Command key on a Mac, so the app closed the focused pane when it saw meta plus W. On a PC, meta is the Windows key. Ctrl+W did not match, fell through to the menu's standard close role, and shut the whole window instead of one pane.
The fix is one function that both the main window and pop-outs now use. It means Command on a Mac and Control everywhere else, and it insists the other one is absent:
function isPrimaryChord(input) { return process.platform === 'darwin' ? (!!input.meta && !input.control) : (!!input.control && !input.meta); }
| Action | Mac | Windows |
|---|---|---|
| Close the focused pane | ⌘W | CtrlWclosed the whole window |
| Cycle panes | ⌘` | Ctrl`accepted either modifier before; now one check |
| Inject into a running turn | CtrlEnter | CtrlShiftEnterso it does not collide with Send now |
| Labels, tooltips, Journal palette | ⌘ ⌥ ⇧ glyphs | Ctrl+ Alt+ Shift+ Winrewritten through one label function |
The last row caught one more corner. The Journal view renders its own tooltips after the app's one-time glyph fixer has already run, so it kept printing ⌘. Every label there now goes through a keyLabel() helper, and a test executes that helper from both the source and the shipped bundle to keep them equal.
A Stop button that stops.
When you press Stop on a Mac, Luminair signals the engine's whole process group: first a polite terminate so tool servers get a clean end of input, then a kill three seconds later for anything still standing. That depends on starting the engine as a detached group leader, which is a POSIX idea.
On Windows the engine now starts attached and hidden (windowsHide), and Stop runs taskkill /PID … /T /F, which walks the tree from the engine down. The comment in the runner explains the order: keep the parent alive until taskkill has enumerated its children, because killing only the engine first can orphan tool servers and shell commands.
The second half of that commit answers a harder question: does the packaged app even start its engines on Windows? A test on a Mac cannot tell you. So build/windows-runtime-probe.js runs inside the packaged Electron on a windows-latest runner. It launches the bundled claude.exe and codex.exe with --version, opens a Windows pseudo-console through the packaged terminal module, and runs from a working folder deliberately named Project space & café, to catch quoting bugs with spaces, ampersands and accents. Keys are blanked, so no account or provider is ever touched.
The probe found its own bug on its second run. Every check passed, then the process refused to exit because the pseudo-console kept its event loop open. The verdict is now the transcript: if it printed WINDOWS_RUNTIME_OK, it passed, however long the child lingers.
An installer that checks its own work.
On 24 September an unsigned test build was installed on Windows 11. The installer reported “Completed”. Every .exe and .dll was gone from the install folder, removed by antivirus as they were written, and the desktop shortcut pointed at nothing.
Release 1.0.210 added two guards the same day. The installer now waits a moment after writing files, then checks for the app executable, ffmpeg.dll, app.asar and the bundled Claude engine. If any is missing it names them, tells you where to look in Windows Security, and fails with a non-zero exit code. And the release workflow refuses to ship unless the installers carry a valid signature and a real silent install keeps every binary.
That evening an arm64 build on a Windows VM showed the same symptom for a different reason. 7-Zip 24 compresses ARM64 binaries with a filter the installer's 7z plugin cannot read, and the plugin skipped those files without an error: all 36 of them, including Luminair.exe and the engine binaries. The build hook now forces the plain BCJ filter, and the next day that was widened from arm64 to every Windows build.
The comparison is strict on purpose. It lists every .exe, .dll and .node file in the unpacked build and requires each one in the installed copy, then uninstalls. An installer that drops files quietly, for any reason, now fails in CI rather than on someone's desk.
Checked, and not claimed.
What this post does not claim
- That every feature works on Windows. The September audit lists what does not exist there yet, including live branch preview, Wake from phone, Desktop Control and bundled offline speech.
- That Luminair supports or blocks any particular Windows version. The dates above are Microsoft's; the antivirus incident we recorded was on Windows 11.
- That the checks run on a Mac prove Windows readiness. The audit notes say the same: only the Windows runner and a real install do.
Sources
- Microsoft SupportWindows 10 support has ended on October 14, 2025
- Tom's Hardware · 14 October 2025Windows 10 support ends today: here's who's affected and what you need to do
- Slashdot · 14 October 2025Windows 10 Support 'Ends' Today
Luminair on Windows
The same sessions, models and Journal, with Ctrl where your fingers expect it.