Skip to content

fix/macos-packaged-readonly-startup#2122

Open
lost-particles wants to merge 3 commits into
PostHog:mainfrom
lost-particles:fix/macos-packaged-readonly-startup
Open

fix/macos-packaged-readonly-startup#2122
lost-particles wants to merge 3 commits into
PostHog:mainfrom
lost-particles:fix/macos-packaged-readonly-startup

Conversation

@lost-particles
Copy link
Copy Markdown

Fixes: #2098

Problem

Packaged PostHog Code users on macOS can end up launching from App Translocation or a read-only volume (e.g. DMG). The app may start partially, then updates, local tasks, or cloud flows misbehave with little in-app explanation. This hurts anyone installing from Downloads, disk images, or other flows that Gatekeeper exposes as a restricted runtime path—not only power users reading main.log.

Closes #2098

Changes

  • Early startup gate (first step inside app.whenReady(), before logs, window creation, or initializeServices()): packaged macOS only. If we detect an unsafe bundle location, show a blocking dialog.showMessageBoxSync explaining read-only/translocation-style behavior, then app.quit().
  • macos-packaged-install-guard.ts: consolidated helpers—
    • AppTranslocation in app.getAppPath() or process.execPath.
    • /sbin/mount output: longest prefix match for resolved exePath; unsafe if mount options contain read-only and mount point is not / (avoids sealed-root false positives).
  • macos-packaged-install-guard.test.ts: table-driven tests for mount parsing and combined isMacosPackagedUnsafeBundleLocation.

No renderer UI screenshots (native Electron dialog only).

How did you test this?

  • Automated: pnpm --filter code exec vitest run src/main/utils/macos-packaged-install-guard.test.ts (all tests passing when run).
  • Manual (recommended): pnpm --filter code package; open PostHog Code.app from a normal writable path (should behave as before); open from a read-only-mounted DMG/volume or a translocated path—expect the blocking dialog and exit after OK. Dev pnpm dev should be unchanged (app.isPackaged).

Fixes: PostHog#2098

Detect App Translocation bundle paths and non-root read-only volumes using
mount(8). Show a blocking dialog before the window or services start.

Ignores read-only  on sealed macOS to avoid false positives.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 11, 2026

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/code/src/main/utils/macos-packaged-install-guard.ts:38
Root mount (`/`) is never selected as `best` because `"${mp}/"` expands to `"//"` and no normal absolute path starts with `"//"`. As a result, the `best.mountPoint === "/"` branch in the caller is dead code — `best` is always `null` for paths that only fall under root. The guard still returns `false` for those paths (via the `!best` branch), so behaviour is correct today, but the explicit root exclusion is never exercised, and any future reader will be misled about what paths actually reach that check.

```suggestion
    const under =
      resolvedPath === mp ||
      resolvedPath.startsWith(mp === "/" ? "/" : `${mp}/`);
```

### Issue 2 of 3
apps/code/src/main/utils/macos-packaged-install-guard.ts:72-75
`execFileSync` is called without a `timeout`. On a machine with a hung NFS or SMB share, `/sbin/mount` can stall indefinitely, freezing app startup — exactly the symptom this PR is trying to cure. Adding a short timeout (e.g. 3 s) ensures the guard degrades gracefully rather than blocking.

```suggestion
    output = execFileSync("/sbin/mount", {
      encoding: "utf8",
      maxBuffer: 10 * 1024 * 1024,
      timeout: 3000,
    });
```

### Issue 3 of 3
apps/code/src/main/utils/macos-packaged-install-guard.test.ts:131-138
**Non-hermetic test for the read-only mount path**

`isMacosPackagedUnsafeBundleLocation` calls the real `/sbin/mount` here. The test passes on CI because `/Volumes/build/out` is not actually mounted anywhere, not because the writable-mount code path was exercised correctly. A machine where that path happened to be mounted read-only would produce a flaky failure. Neither test case in this describe block covers the `isMacosPathOnReadOnlyNonRootMount` branch at all — both exercise only the translocation shortcut or the silent error-catch fallback. Consider adding a test that injects a mock mount table (using `isMacosPathOnReadOnlyNonRootMountFromTable` directly, matching the pattern used in the `isMacosPathOnReadOnlyNonRootMountFromTable` describe block) to prove the read-only detection path works inside `isMacosPackagedUnsafeBundleLocation`.

Reviews (1): Last reviewed commit: "fix/macos-packaged-readonly-startup" | Re-trigger Greptile

Comment thread apps/code/src/main/utils/macos-packaged-install-guard.ts Outdated
Comment thread apps/code/src/main/utils/macos-packaged-install-guard.ts Outdated
Comment thread apps/code/src/main/utils/macos-packaged-install-guard.test.ts Outdated
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 11, 2026

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/code/src/main/utils/macos-packaged-install-guard.ts:25
Mount points whose display names contain ` (` — e.g. `/Volumes/My Backup (2)` — are parsed incorrectly. `indexOf(" (")` stops at the first occurrence inside the name, so `mountPoint` is truncated and the options field picks up the tail of the display name. Using `lastIndexOf` anchors to the options block that macOS always places at the end of the line, matching the `line.endsWith(")")` invariant already enforced above.

```suggestion
    const openParen = afterOn.lastIndexOf(" (");
```

### Issue 2 of 2
apps/code/src/main/utils/macos-packaged-install-guard.test.ts:9-45
The `isMacosAppTranslocationPath` suite has four independent cases with identical structure (two string inputs, one boolean output) but is written as separate `it()` calls. Per the team's style guidance, prefer parameterised tests here — `it.each` would let the signal/noise ratio improve and make adding edge-cases (e.g. case-insensitive segment, partial segment substring) a one-liner. The same applies to `isMacosPathOnReadOnlyNonRootMountFromTable`, where the flat cases on a shared `table` fixture are natural `it.each` rows.

Reviews (2): Last reviewed commit: "minor fixes" | Re-trigger Greptile

const onMarker = line.indexOf(" on ");
if (onMarker === -1) continue;
const afterOn = line.slice(onMarker + 4);
const openParen = afterOn.indexOf(" (");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Mount points whose display names contain ( — e.g. /Volumes/My Backup (2) — are parsed incorrectly. indexOf(" (") stops at the first occurrence inside the name, so mountPoint is truncated and the options field picks up the tail of the display name. Using lastIndexOf anchors to the options block that macOS always places at the end of the line, matching the line.endsWith(")") invariant already enforced above.

Suggested change
const openParen = afterOn.indexOf(" (");
const openParen = afterOn.lastIndexOf(" (");
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/utils/macos-packaged-install-guard.ts
Line: 25

Comment:
Mount points whose display names contain ` (` — e.g. `/Volumes/My Backup (2)` — are parsed incorrectly. `indexOf(" (")` stops at the first occurrence inside the name, so `mountPoint` is truncated and the options field picks up the tail of the display name. Using `lastIndexOf` anchors to the options block that macOS always places at the end of the line, matching the `line.endsWith(")")` invariant already enforced above.

```suggestion
    const openParen = afterOn.lastIndexOf(" (");
```

How can I resolve this? If you propose a fix, please make it concise.

@lost-particles
Copy link
Copy Markdown
Author

@jonathanlab Please review these changes

@charlesvien charlesvien marked this pull request as draft May 18, 2026 05:41
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please parametrize these tests <3

Comment thread apps/code/src/main/index.ts Outdated
"PostHog Code is running from a location with read-only access (for example App Translocation or a read-only disk image). The exact folder can differ depending on how you opened the app.",
detail:
"This prevents updates and tasks from running correctly. Move PostHog Code to the Applications folder (or another folder on a writable disk), quit the app completely, then open it again from the new location.",
buttons: ["OK"],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update "OK" to "Quit"

Comment thread apps/code/src/main/index.ts Outdated
);
dialog.showMessageBoxSync({
type: "warning",
title: "Read-only install location",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read-only install location > Move PostHog Code to Applications

Comment thread apps/code/src/main/index.ts Outdated
type: "warning",
title: "Read-only install location",
message:
"PostHog Code is running from a location with read-only access (for example App Translocation or a read-only disk image). The exact folder can differ depending on how you opened the app.",
Copy link
Copy Markdown
Contributor

@jonathanlab jonathanlab May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just slim it down to
PostHog Code is running from a location with read-only access + The actual bundle root, no need to speculate.

Comment thread apps/code/src/main/index.ts Outdated
message:
"PostHog Code is running from a location with read-only access (for example App Translocation or a read-only disk image). The exact folder can differ depending on how you opened the app.",
detail:
"This prevents updates and tasks from running correctly. Move PostHog Code to the Applications folder (or another folder on a writable disk), quit the app completely, then open it again from the new location.",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets just turn this into "After quitting, move PostHog Code to your Applications folder, then open it from there."

Copy link
Copy Markdown
Contributor

@jonathanlab jonathanlab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing this issue, I've left some comments!

- Parametrize macos-packaged-install-guard tests with it.each
- Dialog: title -> "Move PostHog Code to Applications", show actual
  bundle root in message, slim detail, change button to "Quit"
- parseDarwinMountTable: lastIndexOf(" (") so mount names containing
  " (" (e.g. "My Backup (2)") still parse correctly

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lost-particles lost-particles marked this pull request as ready for review May 18, 2026 19:25
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 18, 2026

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/code/src/main/utils/macos-packaged-install-guard.test.ts:9-45
The four `isMacosAppTranslocationPath` tests exercise the same function signature with different inputs and expected outputs — the classic `it.each` shape. The team always prefers parameterised tests, and the single `it` in `isMacosPathOnReadOnlyNonRootMountFromTable` that bundles two `expect` calls (`/Users/me/app` + `/Applications/Foo.app`) is another instance of the same pattern.

```suggestion
describe("isMacosAppTranslocationPath", () => {
  it.each([
    [
      "returns true when appPath contains AppTranslocation",
      "/private/var/folders/yf/xx/AppTranslocation/C6283C3C-9D6E-4D81-A7D5-8BA2567ED486/d/PostHog Code.app/Contents/Resources/app.asar",
      "/Applications/PostHog Code.app/Contents/MacOS/PostHog Code",
      true,
    ],
    [
      "returns true when exePath contains AppTranslocation",
      "/Applications/PostHog Code.app/Contents/Resources/app.asar",
      "/private/var/folders/yf/xx/AppTranslocation/C6283C3C/d/PostHog Code.app/Contents/MacOS/PostHog Code",
      true,
    ],
    [
      "returns false for normal /Applications paths",
      "/Applications/PostHog Code.app/Contents/Resources/app.asar",
      "/Applications/PostHog Code.app/Contents/MacOS/PostHog Code",
      false,
    ],
    [
      "returns false when neither path is translocated",
      "/Users/dev/PostHog Code.app/Contents/Resources/app.asar",
      "/Users/dev/PostHog Code.app/Contents/MacOS/PostHog Code",
      false,
    ],
  ])("%s", (_, appPath, exePath, expected) => {
    expect(isMacosAppTranslocationPath(appPath, exePath)).toBe(expected);
  });
});
```

Reviews (3): Last reviewed commit: "review fixes: parametrized tests, dialog..." | Re-trigger Greptile

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Detect read-only volume on startup and warn user before app crashes

2 participants