Skip to main content

Electron autoUpdater: Post-Mortem Update Failure Detection

Problem

When a Squirrel.Mac update fails after the app has already quit (e.g. due to TCC permission denial on a non-/Applications volume), there is no way for the app to know what went wrong on next launch. The user just finds their app gone with no explanation.

Root cause in the log:

NSPOSIXErrorDomain Code=1 "Operation not permitted"
NSLocalizedDescription: %APPLICATION_NAME% couldn't be copied because
you don't have permission to access "Applications".

Why Squirrel Cannot Currently Report This

  • ShipIt communicates with Squirrel via a one-way file-based channel (ShipItState.plist). There is no return channel.
  • ShipIt runs after the parent app has quit, so no Squirrel process is alive to receive errors at the time of failure.
  • Squirrel's relaunchToInstallUpdate error signal only covers failures before ShipIt is launched — post-launch failures are invisible to it.
  • ShipIt writes its errors only to ShipIt_stderr.log in the app's ShipIt cache directory.

Proposed Solution

Add post-mortem detection: on next app launch, read the ShipIt stderr log, parse for known failure patterns, and surface them as a structured event that Electron app developers can consume.


Contribution Path

Squirrel/Squirrel.Mac → electron/electron → Apps (VS code, Claude, ...)
(detection) (event API) (UI / dialog)

Each step is an independent PR and can be pursued in parallel.


Step 1 — Squirrel.Mac

Repo: https://github.com/Squirrel/Squirrel.Mac

Add a method to SQRLUpdater.m that runs on app startup:

  1. Locate the ShipIt stderr log via SQRLDirectoryManager (path is already known to Squirrel)
  2. Parse the last Installation error: entry
  3. Detect known failure patterns:
    • NSPOSIXErrorDomain Code=1 → TCC permission denied (EPERM)
    • SQRLCodeSignatureErrorDomain Code=-1 → code signature validation failed
    • SQRLInstallerErrorDomain Code=-7 → cross-volume move failed
  4. Expose a new signal lastInstallError on SQRLUpdater returning a structured NSError with the reason

Key point: This is read-only on startup — no changes to install logic, low risk.


Step 2 — Electron

Repo: https://github.com/electron/electron

Apply a squirrel.patch to the Squirrel.Mac dependency (bypassing the archived fork) and wire the new lastInstallError signal into Electron's autoUpdater module as a new event:

autoUpdater.on('update-failed-last-session', (event, details) => {
// details.reason: 'permission-denied' | 'code-signature' | 'cross-volume'
// details.message: human-readable description
})

Squirrel.patch approach sidesteps the archived fork risk entirely — Electron applies the patch at build time against the upstream Squirrel.Mac source, no fork maintenance required.


Step 3 — Electron Apps

Listen for the new update-failed-last-session event and show a dialog:

autoUpdater.on('update-failed-last-session', (event, details) => {
if (details.reason === 'permission-denied') {
dialog.showMessageBox({
type: 'warning',
title: 'Update Failed',
message: 'The app could not update itself.',
detail:
'The updater (ShipIt) does not have permission to write to your ' +
'Applications folder. Please grant Full Disk Access to ShipIt in ' +
'System Settings to allow future updates.',
buttons: ['Open System Settings', 'Dismiss'],
}).then(result => {
if (result.response === 0) {
shell.openExternal(
'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles'
);
}
});
}
});

Why This Is Low Risk

  • No changes to install logic — detection only
  • ShipIt log path is deterministic, already managed by SQRLDirectoryManager
  • Error patterns are specific and unlikely to produce false positives
  • squirrel.patch approach avoids dependency on the archived Electron fork
  • Each PR is independently mergeable and testable

Failure Patterns to Detect

ErrorDomainCodeReason
TCC permission deniedNSPOSIXErrorDomain1ShipIt needs Full Disk Access
Code signature invalidSQRLCodeSignatureErrorDomain-1Downloaded update is corrupt or tampered
Cross-volume move failedSQRLInstallerErrorDomain-7App on external volume, copy failed

References

  • ShipIt log location: ~/Library/Caches/<bundle-id>.ShipIt/ShipIt_stderr.log
  • Full Disk Access System Settings URL: x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles
  • Squirrel.Mac source: https://github.com/Squirrel/Squirrel.Mac
  • Electron autoUpdater docs: https://www.electronjs.org/docs/latest/api/auto-updater