← Blog Security & Trust 26 September 2026 9 min read

An app that never installs itself

The update channel is the most trusted pipe into your computer, which is why attackers want it. An agent app adds a second risk: agents that can edit the app's own code. Here is how Luminair made sure new code only lands when you say so, and only after it proves it can start.

Fig 01  Four ways the app restarted itself22 to 26 September 2026
ROUTE 23 SEP · DELETED TODAY Installer armed by every stage swapped the app on any quit armed installer update-app.sh arms nothing Waits for your click no consent in 7 days: stays staged Reopen inside the installer open -a after the swap open -a swaps the bundle and exits Reopens only after a card click since 26 Sep; a plain quit never reopens Quit from the staged card quitIntoStagedBuild, applyStaged quitIntoStagedBuild IPC and preload bridge removed The click is the consent writes a marker, then quits Reload every window on save watched renderer.js, index.html, preload.js DEV_SRC_WATCH added 22 Sep 22:56 Gone, and banned by a test which runs before every packaged build
From the commits of 22, 23 and 26 September 2026 and the installer script. Struck-through names were deleted. The shaded boxes are the one kind of route that came back: an action that only happens after you click. This is the staging path the team uses to put new builds on its own Macs; the public release path is covered in section 07.

The short version

  1. 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.
  2. 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.
  3. 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.
  4. 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.
02Built with Luminair

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.

03Updates are a target

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.

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.

04Four ways to restart

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.

05A test, not a comment

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.

desktop/build/beforePack.jsfirst lines
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.

Why a test beats a ruleA rule in a document asks the next person, or the next agent, to remember. A test in the build refuses to produce the app at all. Agents are very good at following a failing test to its cause.
06What an install must prove

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.

Fig 02  Checks around the swapdesktop/install-staged-update.sh
BEFORE THE SWAP Still the verified buildversion, path and SHA-256 of app.asar match the marker Signed by uscodesign --strict, and the expected Developer ID Can launchengines executable, archive matches the Info.plist seal Old app is healthya corrupt outgoing app is never kept as the rollback The swap two renames, with TERM, INT and HUP ignored so nothing can stop it halfway AFTER THE SWAP Signature againthe installed copy, not the staged one Launch proof--ow-selftest must reach ready within 90 s any failure: move the new app aside, restore the previous one Lock itevery file under Contents flagged immutable (uchg) Keeps the two newest rollbacks and three failed installs; everything older is pruned.
The real order of checks in the staging installer, shortened to one line each. Filled dots are checks that can abort or roll back; the hollow dot is the final lock. The launch proof starts the installed app with a self-test flag that exits the moment the app is ready.

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.

07The public release

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.

Fig 03  Two ways new code reaches a MacFrom the scripts and workflow
Public release (users)Staging (the team's Macs)
Built byOne release workflow, draft firstA local script, many times a day
IntegrityEvery feed digest must match the draft's bytesSHA-256 of the archive pinned at staging, rechecked at install
SignatureSigned Mac builds; Windows installers must pass AuthenticodeStrict codesign with the expected Developer ID
DownloadsIn the background, as soon as a release is seenAlready on disk
InstallsWhen you quit, or when you click the cardOnly after you click the staged card
ReopensWhen you clicked to restartOnly after that click
A summary of the two paths as they stand in the source. Neither path reloads a window or relaunches the app on its own initiative.
08What you see

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.

Sidebar · update card

1 · Seen

Downloading update v…percent while it downloads

2 · Downloaded

Ready to Installclick to restart now (or it installs on next launch)

3 · Clicked

Updating…Restarting
Redrawn with the card's text from the renderer source; the version number is left out on purpose. The ✕ on the card skips that version until the next build.
09What we checked

Checked, and not claimed.

12/12
No-self-reload and launch-continuity tests pass; the first file runs before every packaged build
4
Routes to a self-restart deleted on 23 September 2026
90 s
For a newly installed app to prove it starts, or it is rolled back
7 days
An armed installer waits for a click before leaving the build staged and exiting

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

Updates on your schedule

Luminair downloads in the background and waits for you to quit or click.

Download Luminair →