fix/macos-packaged-readonly-startup#2122
Conversation
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.
Prompt To Fix All With AIFix 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 |
Prompt To Fix All With AIFix 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(" ("); |
There was a problem hiding this 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.
| 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.|
@jonathanlab Please review these changes |
There was a problem hiding this comment.
Please parametrize these tests <3
| "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"], |
There was a problem hiding this comment.
Please update "OK" to "Quit"
| ); | ||
| dialog.showMessageBoxSync({ | ||
| type: "warning", | ||
| title: "Read-only install location", |
There was a problem hiding this comment.
Read-only install location > Move PostHog Code to Applications
| 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.", |
There was a problem hiding this comment.
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.
| 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.", |
There was a problem hiding this comment.
Lets just turn this into "After quitting, move PostHog Code to your Applications folder, then open it from there."
jonathanlab
left a comment
There was a problem hiding this comment.
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>
Prompt To Fix All With AIFix 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 |
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
app.whenReady(), before logs, window creation, orinitializeServices()): packaged macOS only. If we detect an unsafe bundle location, show a blockingdialog.showMessageBoxSyncexplaining read-only/translocation-style behavior, thenapp.quit().macos-packaged-install-guard.ts: consolidated helpers—AppTranslocationinapp.getAppPath()orprocess.execPath./sbin/mountoutput: longest prefix match for resolvedexePath; unsafe if mount options containread-onlyand mount point is not/(avoids sealed-root false positives).macos-packaged-install-guard.test.ts: table-driven tests for mount parsing and combinedisMacosPackagedUnsafeBundleLocation.No renderer UI screenshots (native Electron dialog only).
How did you test this?
pnpm --filter code exec vitest run src/main/utils/macos-packaged-install-guard.test.ts(all tests passing when run).pnpm --filter code package; openPostHog Code.appfrom 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. Devpnpm devshould be unchanged (app.isPackaged).