The short version
- In 2025 attackers hijacked Notepad++'s update traffic for months and served some users a malicious update. The fix there was to verify signatures on what the updater downloads.
- An agent app has a second exposure: the agents running inside it can edit files, including the app's own. In September 2026 that produced a screen that reloaded itself whenever an agent saved a file.
- On 23 September Luminair deleted all four routes by which it could relaunch, install or reload itself, and a test that bans them now runs before every packaged build.
- A new build replaces the old one only after it passes signature, integrity and launch checks, and the public release is not published until every file matches its digest and the Windows installers are signed and proven by a real install.
The bugs came from our own agents.
Luminair is built inside Luminair, by agents that read and write the repository all day. That is how this post's two worst bugs happened, and both are written down in the code that fixed them.
On 1 September an AI session running inside the app extracted the installed app's archive, repacked it, copied it back over the installed one and re-signed it. The seal in the app's Info.plist no longer matched the archive, so Electron refused to start the app at all. On 22 September a file watcher was added that reloaded every open window whenever the renderer source was saved. From then on, any agent editing the interface wiped the screen of the person using it, mid-sentence, with nothing to tell it apart from a crash.
Both fixes, and the tests that hold them, were also written in Luminair sessions. This post was drafted the same way, from the repository and its history.
The most trusted pipe on your computer.
On 2 February 2026 the Notepad++ project published Notepad++ Hijacked by State-Sponsored Hackers. The attack did not touch the editor's code. It compromised the shared hosting server behind the update service, and “Traffic from certain targeted users was selectively redirected to attacker-controlled malicious update manifests.” The project estimates the compromise ran from June to 2 December 2025.
BleepingComputer and Help Net Security covered the fallout. Help Net Security quotes researcher Kevin Beaumont on the victims: “Activity appears very targeted.” The hosting provider's statement to the project says the attackers searched specifically for the Notepad++ domain, and may have known of weaknesses “related to insufficient update verification controls” in older versions.
The remedy is the lesson. Since version 8.8.9 the Notepad++ updater verifies “both the certificate and the signature of the downloaded installer”, and the update manifest itself is now signed. In other words: never trust the pipe, trust the signature on what comes out of it.
A desktop agent app has one more pipe to think about. Its agents run shell commands as you. If they can write into the installed app, then every bug, every bad instruction and every injected prompt is also a potential update channel.
Deleted, not gated.
The team ships new builds to its own Macs many times a day through a staging path: a script builds, verifies and signs a new app bundle and parks it beside the installed one. By September that path had grown four ways to make the app close and reopen without being asked, listed in Fig 01. The commit of 23 September explains why it deleted them outright instead of putting them behind a setting: “an opt-in flag still leaves the behaviour one mistake away.”
After the purge, staging stages and nothing more. The installer swaps the bundle and exits. The sidebar's staged card stayed clickable, but for about a day its click only copied the install command, so the install happened in a terminal, on purpose, with the app quit.
Then one route came back, deliberately and narrowly. Clicking the staged card now writes a consent marker and quits the app. The waiting installer sees the marker, installs, and, since 26 September, reopens the app only if the marker says it came from that click. A plain quit, a crash or a new stage never reopens anything. A stale click never consents to a newer build, because staging deletes the old marker, and an installer that gets no click within seven days leaves the build staged and exits.
The fix existed only in the repository.
The reload watcher is the reason this part is strict. It was removed at 06:53 on 23 September, but a build made before the fix kept reloading screens for hours. A comment saying “never do this again” could not stop it coming back, and could not prove anything about a build already on disk. So the ban became a test file, no-self-reload.test.js, with a header that tells the whole story.
exports.default = async function beforePack(context) { execFileSync(process.execPath, ['--test', 'test/core/no-self-reload.test.js'], { cwd: path.resolve(__dirname, '..'), stdio: 'inherit', }); // ...
It runs as the first step of every packaged build, before anything is copied. Its assertions are deliberately blunt. There may be exactly one reloadIgnoringCache in the main process, and only on the menu-bar panel. A plain reload() is allowed only inside the handler for a renderer that has already crashed. No fs.watch may point at a source checkout, and the old watcher's identifiers are named so a copy-paste revival fails loudly. The staging script may only ever kill a stray installer, never the app, and may not reopen it.
It has to start before it stays.
Consent decides when a build installs. It does not make the build safe. The installer script checks a chain of facts before and after the swap, and rolls back if any of them fails.
Two details came straight from the 1 September incident. The immutable flag means a cp, mv, chmod or codesign onto any file inside the installed app now fails with a permission error instead of silently breaking it; a real install never needs that, because it renames whole bundles. And in the Claude SDK lane, a hook refuses any shell command that would write into the installed bundle, records a bundle.write_blocked event, and tells the agent why: “Luminair refuses to modify its own installed bundle”, so edit the source tree and stage a new build instead. The command-line lanes are stopped by the flag itself.
Retention came from the same audit: 67 superseded bundles, about 70 GB, had never been pruned.
Digests, signatures, and a real install.
Users do not get builds through the staging path. They get releases from one workflow, which builds into a draft, verifies it, and only then publishes. The update feed lists a SHA-512 digest and size for every file, and a seal step refuses to publish if any entry does not match the bytes actually in the draft.
That check caught a quiet problem on 7 September. A release built only one Mac architecture and carried the other forward from the previous release. The seal failed: “Feed digest mismatch”. The old feed had been written before the disk image was notarized and stapled, and stapling makes the file bigger. For v1.0.99 on Intel, the feed said 421,843,067 bytes and the asset was 421,855,301. The fix recomputes the carried feed's digests from the carried bytes, and the seal then checks them like any others.
Recomputing a digest proves the feed matches the file. It does not prove the file is ours; that is the signature's job. On Windows, since 24 September, the release workflow checks that both installers and the app executable carry a valid Authenticode signature, then runs a real silent install and requires every binary from the build to be present on disk. An unsigned installer, or one that installs with missing files, stops the release.
| Public release (users) | Staging (the team's Macs) | |
|---|---|---|
| Built by | One release workflow, draft first | A local script, many times a day |
| Integrity | Every feed digest must match the draft's bytes | SHA-256 of the archive pinned at staging, rechecked at install |
| Signature | Signed Mac builds; Windows installers must pass Authenticode | Strict codesign with the expected Developer ID |
| Downloads | In the background, as soon as a release is seen | Already on disk |
| Installs | When you quit, or when you click the card | Only after you click the staged card |
| Reopens | When you clicked to restart | Only after that click |
Three states, one click.
“Never installs itself” needs one honest footnote. When a release has been downloaded, Luminair installs it the next time you quit, so your next launch runs it. What it does not do is interrupt you: no reload, no relaunch, no install while you are working, and never a version you chose to skip. The card in the sidebar tells you where things stand.
1 · Seen
Downloading update v…percent while it downloads2 · Downloaded
Ready to Installclick to restart now (or it installs on next launch)3 · Clicked
Updating…RestartingChecked, and not claimed.
What this post does not claim
- That Luminair is immune to an attack on its release infrastructure. Signatures and digests raise the cost; they are not a proof.
- That a downloaded release never installs without a click. It installs when you quit, by design, as described above.
- That every agent command touching the app is caught. The hook covers the Claude SDK lane; the other lanes rely on the immutable flag.
Sources
- Notepad++ · 2 February 2026Notepad++ Hijacked by State-Sponsored Hackers
- BleepingComputer · 2 February 2026Notepad++ update feature hijacked by Chinese state hackers for months
- Help Net Security · 2 February 2026How state-sponsored attackers hijacked Notepad++ updates
Updates on your schedule
Luminair downloads in the background and waits for you to quit or click.