Skip to content

[Bug]: macOS automatic updates can lose or fail to replace the app bundle when installed on an external drive #31244

Description

@atoz03

Before submitting

  • 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

  1. Copy T3 Code.app to a writable external drive, for example /Volumes/ExternalDrive/T3 Code.app, and open it from there.
  2. Let an automatic update download, then choose to restart and install it (quitAndInstall).
  3. 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.

Impact

Major degradation or frequent failure

Version or commit

T3 Code (Nightly) 0.1.8-nightly; main @ 8b44f42

Environment

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).

Activity

  1. added a commit that references this issue on Nov 5, 2026
    4822105
  2. atoz03 commented on Nov 5, 2026

    @atoz03
    Author

    PR #31248 already implements the full fix covering all four cases:

    1. 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.
    2. 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.
    3. 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.
    4. 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.

  3. added a commit that references this issue on Nov 5, 2026
    208f009
  4. atoz03 commented on Nov 5, 2026

    @atoz03
    Author

    Update on #31248, now at 2a293e5:

    • 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.
  5. added a commit that references this issue on Nov 5, 2026
    299ae53
  6. shivamhwp commented on Nov 5, 2026

    @shivamhwp
    Collaborator

    [GPT-6.1 Sol][Codex] on behalf of shivam.

    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.

  7. deleted a comment from Wladefant on Nov 8, 2026
  8. deleted a comment from Wladefant on Nov 8, 2026
  9. atoz03 commented on Nov 12, 2026

    @atoz03
    Author

    @juliusmarminge @maria-rcks

    I’m frustrated that this issue still has no clear owner or resolution after I reported the bug, submitted #31248, addressed the review findings, and provided regression tests and verification evidence.

    As of today, automatic updates still do not work for my external-drive installation. I have to replace the app manually for every update, and I’m frustrated by this recurring inconvenience.

    The automated handoff above points to #31261, but it explicitly acknowledges that #31261 only covers read-only locations and leaves writable external-drive installs unchanged. The interrupted-update failure on a writable external drive is part of this issue. That handoff does not resolve it.

    PR #31248 addresses the writable external-drive case as well. Its branch has been updated with main and all 22 checks are green, but there is still no clear maintainer decision.

    Please clarify who is responsible for taking this issue forward and what the next step is:

    I’m willing to work through concrete feedback. Leaving the issue open after a handoff that does not cover the full problem, with no clear next step, is not an adequate resolution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions