You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: macOS automatic updates can lose or fail to replace the app bundle when installed on an external drive #31244
I searched existing issues and did not find a duplicate.
I included enough detail to reproduce or investigate the problem.
Area
apps/desktop
Steps to reproduce
Copy T3 Code.app to a writable external drive, for example /Volumes/ExternalDrive/T3 Code.app, and open it from there.
Let an automatic update download, then choose to restart and install it (quitAndInstall).
Let ShipIt be stopped partway through the install. On macOS 27 this happens when macOS terminates ShipIt after the app quits (Squirrel/Squirrel.Mac#336). It also happens when the drive sleeps or is disconnected during the install.
A second case: open T3 Code straight from the mounted disk image, or from Downloads so that macOS runs it from App Translocation, then try to install an update.
Expected behavior
The copy on the external drive is replaced in one step by the new version and relaunches. If the app cannot replace itself where it runs (disk image, read-only drive, App Translocation), automatic updates are off and Settings explains why. They should not fail at install time.
Actual behavior
Squirrel.Mac's ShipIt stages its work in NSTemporaryDirectory() on the startup disk (-[SQRLInstaller ownedTemporaryDirectoryURL]). When the app lives on another volume, $TMPDIR and the app have different st_dev. Moving the old bundle out and the new bundle in then fails rename() with EXDEV, and ShipIt falls back to copying the whole bundle across volumes and deleting the source. That is slow, and the install is no longer atomic. If ShipIt is stopped partway, the external drive is left with the app missing or half-written.
From App Translocation or a read-only disk image, the app still checks for and downloads updates. The install fails only inside quitAndInstall, after T3 Code has already stopped its backends.
The cross-volume failure mode above comes from reading Squirrel.Mac's SQRLInstaller.m. We do not have a ShipIt log from an interrupted install.
macOS 27.2, Apple Silicon (arm64). T3 Code (Nightly) desktop app on a writable APFS external volume, updated by the bundled Squirrel.Mac / electron-updater.
Logs or stack traces
# st_dev from stat on macOS 27.2
/ 16777231
$TMPDIR 16777231
mounted APFS volume 16777239
Workaround
Move T3 Code to /Applications on the startup disk and open it from there.
Model and harness: Claude Opus 5.5 (Claude Agent), reviewed by GPT-6.1 Sol (Codex).
Worker16 pre-PR source scope note — writable external-volume case remains separate
Receiving task 6ae9048924888191ba619eb5249ca846, invocation 41aa7418-87e0-832d-9f2c-2d89601424b5, setup HL-SB-20261102. This is a source-derived scope note, not a formal PR review; the author retains branch fix/issue-31244.
At the fresh branch HEAD 066595809345b9712c96c18dae109c57ad32ef6c (3 commits ahead of main@935818e422f9b58c46e14d70f4ef386fa3575105), the three-file delta detects only EROFS from a writable-access probe on the app bundle. It correctly disables updates for read-only disk images/App Translocation and preserves the EACCES case, where ShipIt may obtain authorization. But a writable app bundle on a different mounted volume passes that probe: isReadOnlyMacBundle stays false, so automatic updates remain enabled. The branch does not move ShipIt's staging directory onto the app volume or otherwise change its cross-device replacement behavior.
That leaves the issue's first scenario unresolved: Squirrel.Mac stages under NSTemporaryDirectory() on the startup volume; when the app is on another writable volume, the rename is cross-device (EXDEV) and falls back to a non-atomic copy/delete path. The report explicitly says there is no ShipIt log from an interrupted install, so the partial-bundle outcome is not independently observed here; this is a source/issue-derived residual, not a reproduced incident.
Requested scope decision before PR: either keep the candidate narrowly scoped to read-only/translocated bundles and split/retain the writable external-volume atomicity problem as a separate tracked fix, or add an original-scenario regression and a same-volume staging/atomic-replacement correction. Acceptance should cover (1) writable external app volume distinct from temp volume, (2) read-only EROFS disables before download/install, (3) EACCES remains eligible for normal authorization, and (4) ordinary writable startup-volume installs remain enabled. Do not mark the full #31244 problem fixed from the EROFS guard alone.
No source/branch/CI/host/installation/merge operation was performed.
PR #31248 already implements the full fix covering all four cases:
Writable external volume (bundle.dev !== temp.dev): stages the downloaded zip beside the app on the same volume (.T3 Code.update/extracted), verifies the extracted bundle against the running app's designated code-signing requirement via /usr/bin/codesign, and swaps the bundle in place using same-volume mv renames with rollback and signal hardening.
Read-only volumes (mounted DMG / read-only external drive) & App Translocation: disables automatic updates upfront before download/install and surfaces the reason in Settings → About.
Startup-volume permission checks (EACCES): startup-disk installs (bundle.dev === temp.dev, including /Applications and /Volumes/Macintosh HD) return ShipIt directly without a writable-access probe, preserving Squirrel.Mac's normal authorization flow.
Ordinary startup-volume installs: remain enabled and continue using electronUpdater.quitAndInstall unchanged.
Regression tests for all four cases are included in apps/desktop/src/updates/DesktopUpdates.test.ts and apps/desktop/src/updates/MacAppVolume.test.ts.
Changed-head review for linked PR #31248: review #5779911530 was submitted and read back as CHANGES_REQUESTED on exact head 7f7056d (base main@298cb24140fdddebfbfaedad3a097e86ff3d9d04).
Remaining source findings:
P2: the default 300 × 0.2s retry budget is shared with app-exit waiting. If the external volume remains unavailable beyond the remaining budget, the detached helper exits while the app path is absent and previous.app remains only in .T3 Code.update. A later remount/reboot has no recovery actor; the app cannot launch to run app-level recovery. The new negative test proves backup preservation, but explicitly leaves the target absent. Require an atomic swap or recovery that survives helper timeout/restart, with a delayed-remount regression.
P3: the helper deletes the stage/previous.app before open -n succeeds. A failed Launch Services launch therefore loses the rollback copy. Keep the backup through launch and test the failed-open restore path.
Exact PR-event CI was read, not dispatched: workflow #41967200064 tested merge candidate c9de435 (head 7f7056d + base 298cb24); MacAppVolume.test.ts 2/2 and DesktopUpdates.test.ts 43/43 passed, with Lint, Build/Typecheck and overall CI successful. Full packaged in-app external-volume update, exFAT/HFS+ and Intel remain NOT RUN.
Owner/next action: atoz03 retains fix/macos-external-volume-updates and all PR paths; address the P2 and P3 on a new normal head, then obtain exact-head CI and a separate changed-head review. This comment records source qualification only, not a source write, merge, installation or host acceptance.
P2: the app-exit wait and the post-move retry budget are separate. A rerun over a stage holding previous.app finishes the swap, and launching from .T3 Code.update/previous.app (or from the bundle path with the bundle missing) puts the app back. Regression tests cover the drive returning after the helper exited. The swap is still two renames, not an atomic exchange.
P3: previous.app is kept until open -n succeeds and is restored and reopened if open fails. A test covers it.
Issue watch is handing this issue off because Julius owns #31261, which has a closing reference to it. At head 8da4750, its description explicitly limits the fix to read-only locations and leaves writable external-drive installs unchanged. The separate external-volume implementation is in #31248. No code changed or device tests ran in this automation thread; the interrupted external-volume install remains unverified here.
Activity
fix(desktop): stage macOS updates on the app's own volume for externa…
Wladefant commentedon Nov 5, 2026
Worker16 pre-PR source scope note — writable external-volume case remains separate
Receiving task
6ae9048924888191ba619eb5249ca846, invocation41aa7418-87e0-832d-9f2c-2d89601424b5, setupHL-SB-20261102. This is a source-derived scope note, not a formal PR review; the author retains branchfix/issue-31244.At the fresh branch HEAD
066595809345b9712c96c18dae109c57ad32ef6c(3 commits ahead ofmain@935818e422f9b58c46e14d70f4ef386fa3575105), the three-file delta detects onlyEROFSfrom a writable-access probe on the app bundle. It correctly disables updates for read-only disk images/App Translocation and preserves theEACCEScase, where ShipIt may obtain authorization. But a writable app bundle on a different mounted volume passes that probe:isReadOnlyMacBundlestays false, so automatic updates remain enabled. The branch does not move ShipIt's staging directory onto the app volume or otherwise change its cross-device replacement behavior.That leaves the issue's first scenario unresolved: Squirrel.Mac stages under
NSTemporaryDirectory()on the startup volume; when the app is on another writable volume, the rename is cross-device (EXDEV) and falls back to a non-atomic copy/delete path. The report explicitly says there is no ShipIt log from an interrupted install, so the partial-bundle outcome is not independently observed here; this is a source/issue-derived residual, not a reproduced incident.Requested scope decision before PR: either keep the candidate narrowly scoped to read-only/translocated bundles and split/retain the writable external-volume atomicity problem as a separate tracked fix, or add an original-scenario regression and a same-volume staging/atomic-replacement correction. Acceptance should cover (1) writable external app volume distinct from temp volume, (2) read-only EROFS disables before download/install, (3) EACCES remains eligible for normal authorization, and (4) ordinary writable startup-volume installs remain enabled. Do not mark the full #31244 problem fixed from the EROFS guard alone.
No source/branch/CI/host/installation/merge operation was performed.
atoz03 commentedon Nov 5, 2026
PR #31248 already implements the full fix covering all four cases:
bundle.dev !== temp.dev): stages the downloaded zip beside the app on the same volume (.T3 Code.update/extracted), verifies the extracted bundle against the running app's designated code-signing requirement via/usr/bin/codesign, and swaps the bundle in place using same-volumemvrenames with rollback and signal hardening.EACCES): startup-disk installs (bundle.dev === temp.dev, including/Applicationsand/Volumes/Macintosh HD) returnShipItdirectly without a writable-access probe, preserving Squirrel.Mac's normal authorization flow.electronUpdater.quitAndInstallunchanged.Regression tests for all four cases are included in
apps/desktop/src/updates/DesktopUpdates.test.tsandapps/desktop/src/updates/MacAppVolume.test.ts.fix(desktop): stage macOS updates on the app's own volume for externa…
Wladefant commentedon Nov 5, 2026
Changed-head review for linked PR #31248: review #5779911530 was submitted and read back as CHANGES_REQUESTED on exact head 7f7056d (base main@298cb24140fdddebfbfaedad3a097e86ff3d9d04).
Remaining source findings:
Exact PR-event CI was read, not dispatched: workflow #41967200064 tested merge candidate c9de435 (head 7f7056d + base 298cb24); MacAppVolume.test.ts 2/2 and DesktopUpdates.test.ts 43/43 passed, with Lint, Build/Typecheck and overall CI successful. Full packaged in-app external-volume update, exFAT/HFS+ and Intel remain NOT RUN.
Owner/next action: atoz03 retains fix/macos-external-volume-updates and all PR paths; address the P2 and P3 on a new normal head, then obtain exact-head CI and a separate changed-head review. This comment records source qualification only, not a source write, merge, installation or host acceptance.
atoz03 commentedon Nov 5, 2026
Update on #31248, now at 2a293e5:
previous.appfinishes the swap, and launching from.T3 Code.update/previous.app(or from the bundle path with the bundle missing) puts the app back. Regression tests cover the drive returning after the helper exited. The swap is still two renames, not an atomic exchange.previous.appis kept untilopen -nsucceeds and is restored and reopened ifopenfails. A test covers it.fix(desktop): stage macOS updates on the app's own volume for externa…
shivamhwp commentedon Nov 5, 2026
Issue watch is handing this issue off because Julius owns #31261, which has a closing reference to it. At head 8da4750, its description explicitly limits the fix to read-only locations and leaves writable external-drive installs unchanged. The separate external-volume implementation is in #31248. No code changed or device tests ran in this automation thread; the interrupted external-volume install remains unverified here.