feat(desktop): import cookies from Safari#7262

merged21
main
browser-import-safari
openedAug 16, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 3d ago

Adds Safari as a browser import source on macOS.

Safari keeps its cookies unencrypted in Cookies.binarycookies; the protection is Full Disk Access (TCC), not a key. The reader parses the binary format with every declared structure bounds-checked and refuses a jar the page table does not describe rather than importing part of it. Timestamps are rebased from the 2001 epoch, and host-only vs domain cookies go through the same cookieScope rule as the Chromium and Firefox readers.

TCC denies the read with EPERM while still allowing stat, so listing probes the jar by opening it and reports needsFullDiskAccess up front. The wizard gains a Full Disk Access step: Open System Settings deep-links to the Privacy & Security โ†’ Full Disk Access pane through a dedicated openSystemSettings IPC method keyed by pane id (the generic openExternal allowlist is unchanged), and after granting the wizard rechecks or retries the selected import. Only Safari's primary profile jar is read; additional Safari profiles are out of scope.

Validation: binary parser fixtures and malformed-record boundaries, an EPERM-vs-EACCES distinction test, the fixed System Settings destination, and wizard state transitions. The parser was also run against reference binarycookies fixtures and structurally checked against a live Safari 27 jar.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents via Codex, and Claude Fable 5 via Claude Code.

Note

Add Safari cookie import with Full Disk Access permission flow

  • Adds Safari as a Darwin-only browser import source that reads Safari's binary cook cookie jar instead of SQLite databases used by Chromium and Firefox
  • Implements parseBinaryCookies in SafariCookies.ts to decode the big-endian page table, little-endian records, Safari epoch timestamps, URL/domain/path scope, and secure/HTTP-only flags; structurally invalid jars fail with SafariCookieReadError
  • Discovers named Safari profiles from SafariTabs metadata with fallback to scanning UUID-named WebKit data stores when metadata is unreadable
  • Adds a needsFullDiskAccess unavailability reason and a wizard step that lets users open the macOS Full Disk Access System Settings pane, recheck permission, and resume the import
  • Safari source listing returns needsFullDiskAccess when the cookie jar exists but opening it yields EPERM; EACCES and other errors remain generic read failures

Macroscope summarized c6cb164.


Note

Medium Risk Touches macOS TCC-sensitive cookie reads and new IPC that opens fixed System Settings URLs; parser mistakes could drop or mis-import cookies, but URLs are allowlisted and malformed jars fail closed.

Overview Adds Safari as a macOS browser-import source, reading Cookies.binarycookies with a bounds-checked parser that maps records into preview cookies (epoch rebase, host-only/cookieScope, secure/httpOnly flags) and surfaces TCC denials as needsFullDiskAccess (EPERM) rather than generic read failures.

Availability and import probe Safari by opening the jar (since stat can succeed without Full Disk Access), wire the Safari engine through BrowserImport, and extend source discovery to list the default jar plus named profiles from SafariTabs.db / WebKit data stores, resolving Cookies.binarycookies per selected profile. Safari is treated as safe to import while running (no lock probe).

Desktop shell / UX: new openSystemSettings("full-disk-access") IPC (pane id, not renderer URLs) deep-links to Privacy & Security โ†’ Full Disk Access; the import wizard gains a Full Disk Access step with recheck/retry, and Integrations calls the local API to open settings. Contracts add the safari source id and needsFullDiskAccess copy; user docs note the FDA flow.

Reviewed by Cursor Bugbot for commit c6cb164bc007a02da724bb446fa2d1efceb1b8b5. Bugbot is set up for automated code reviews on this repo. Configure here.

Synced from GitHub

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

โš™๏ธ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ea8679c-247d-4066-a7c4-bc7ad8914eb6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ๐Ÿ” Trigger review

Comment @coderabbitai help to get the list of available commands.

apps/desktop/src/preview/BrowserImport/SafariCookies.tslines 65โ€“66Outdated ยท 88cf9f7Open thread
โ€ฆโ€ฆ@@ -0,0 +1,130 @@
63+ let pageStart = 8 + pageCount * 4;
64+
65+ for (const pageSize of pageSizes) {
66+ const page = buffer.subarray(pageStart, pageStart + pageSize);
MmacroscopeappBoton lines 65โ€“66Aug 16, 2026

๐ŸŸก Medium BrowserImport/SafariCookies.ts:65

A truncated page is accepted as a successful import, so cookies after the truncated boundary are silently omitted. Buffer.subarray clamps pageStart + pageSize to the available buffer length; validate the declared page range and throw SafariCookieReadError("readFailed") before creating the page slice.

โœ… Resolved in 3df1bbe3d13a59d55a7fd6213ee9d1e27e66bfbd

Synced from GitHub

Thread transfer impact

โœ… Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.8 KiB+225 B (+1.6%)15.1 KiBโœ…
CodexThread snapshot wire7.0 KiB7.0 KiBโˆ’1 B (โˆ’0.0%)7.3 KiBโœ…
CodexLive turn WebSocket wire6.5 KiB6.7 KiB+226 B (+3.4%)7.8 KiBโœ…
CodexLive turn WebSocket decoded57.0 KiB58.5 KiB+1.5 KiB (+2.6%)66.4 KiBโœ…
CodexLive turn messages810+2 (+25.0%)21โœ…
ClaudeTotal thread wire13.7 KiB13.6 KiBโˆ’172 B (โˆ’1.2%)15.1 KiBโœ…
ClaudeThread snapshot wire7.0 KiB7.0 KiB+4 B (+0.1%)7.3 KiBโœ…
ClaudeLive turn WebSocket wire6.7 KiB6.5 KiBโˆ’176 B (โˆ’2.6%)7.8 KiBโœ…
ClaudeLive turn WebSocket decoded59.3 KiB57.8 KiBโˆ’1.5 KiB (โˆ’2.5%)66.4 KiBโœ…
ClaudeLive turn messages109โˆ’1 (โˆ’10.0%)21โœ…

Baseline: 5e1c23d ยท PR result: c6cb164 ยท Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 113.8 KiB
  • Claude decoded thread snapshot: 114.5 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

apps/desktop/src/preview/BrowserImport/SafariCookies.tsline 114Outdated ยท 31db6abOpen thread
โ€ฆโ€ฆ@@ -0,0 +1,146 @@
111+ // The format predates SameSite and carries no equivalent field. Lax is
112+ // the modern browser default; claiming "none" would widen every
113+ // imported cookie's scope.
114+ sameSite: "lax",
CcursorBoton line 114Aug 16, 2026

SameSite flags ignored

Medium Severity

Safariโ€™s cookie flags field encodes SameSite in bits 3โ€“5 (0x20 none, 0x28 lax, 0x38 strict), but every imported cookie is hard-coded to lax. Cross-site None sessions can fail after import, and Strict cookies are widened to lax.

Reviewed by Cursor Bugbot for commit 31db6ab42c3d74b427087748350a1f7156043b26. Configure here.

Leaving lax. The reference descriptions of bits 3โ€“5 disagree (creachadair's gist says 4/5/7 = None/Lax/Strict; interstateone/BinaryCookies calls them unknown), and the histogram of those bits on a real Safari 27 jar does not match either encoding cleanly. Guessing wrong toward none widens every cookie; lax is the browser default. Corrected the code comment in 9611ffbea so it no longer claims the format has no such field.

apps/desktop/src/preview/BrowserImport/SafariCookies.tsline 134Outdated ยท 31db6abOpen thread
โ€ฆโ€ฆ@@ -0,0 +1,146 @@
131+ return new SafariCookieReadError({
132+ reason: denied ? "needsFullDiskAccess" : "readFailed",
133+ cause,
134+ });
CcursorBoton line 134Aug 16, 2026

TCC denial misdetected

High Severity

needsFullDiskAccess is only returned when Effect reports PermissionDenied, which maps from EACCES. macOS TCC blocks on Cookies.binarycookies with EPERM, which Effect treats as Unknown, so real Full Disk Access denials surface as generic readFailed instead of the FDA remedy. The test covers chmod (EACCES), not TCC.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 31db6ab42c3d74b427087748350a1f7156043b26. Configure here.

Synced from GitHub

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable โ€” This PR adds a substantial macOS Safari cookie-import workflow that reads protected browser data, transfers authentication cookies, and introduces new parser, permission, UI, and IPC behavior. It also adds a static-analysis suppression directive, so the scope and review-sensitive changes require human review.

Not approved because:

  • Per-PR cost limit exceeded (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings, or comment @macroscope-app review this PR to bypass the limit and review now. You can add or adjust custom eligibility rules. Learn more.

MmacroscopeappBotreviewed ยท 1 inline threadAug 16, 2026
Synced from GitHub

Effect service conventions review: one finding on the new Safari cookie error. Everything else (subpath namespace imports, Effect.fn boundaries, tagged-error definition, pass-through of the already structured error in Effect.try, no hidden runtimes) matches the conventions.

Posted via Macroscope โ€” Effect Service Conventions

apps/desktop/src/preview/BrowserImport/SafariCookies.tslines 45โ€“47Outdated ยท d66d4fbOpen thread
โ€ฆโ€ฆ@@ -0,0 +1,179 @@
44+ {
45+ reason: SafariCookieReadFailure,
46+ /** Kept for the log; never surfaced to the user. */
47+ cause: Schema.optional(Schema.Defect()),
MmacroscopeappBoton lines 45โ€“47Aug 16, 2026

BrowserImport/SafariCookies.ts:47

SafariCookieReadError carries only reason and cause, so a TCC denial and a corrupt jar both log without naming the file that was read. The path is available at every construction site (readSafariCookies receives cookiePath, and parseBinaryCookies can take it as a parameter), and the sibling ChromiumCookieReadError already carries cookieDatabasePath for exactly this reason. Consider adding a cookiePath: Schema.String structural attribute and deriving the message from it, e.g. Could not read Safari cookies at ${this.cookiePath}: ${this.reason}.

No longer relevant as of a8129c16f241155f34b7753fffe6c2f2d989201c

MmacroscopeappBotreviewed ยท 1 inline threadAug 16, 2026
Synced from GitHub

Effect service conventions review: one finding on the new Safari cookie error. Everything else (subpath namespace imports, Effect.fn boundaries, tagged-error definition, pass-through of the already structured error in Effect.try, no hidden runtimes) matches the conventions.

Posted via Macroscope โ€” Effect Service Conventions

apps/desktop/src/preview/BrowserImport/SafariCookies.tslines 45โ€“47Outdated ยท 06d5c22Open thread
โ€ฆโ€ฆ@@ -0,0 +1,179 @@
44+ {
45+ reason: SafariCookieReadFailure,
46+ /** Kept for the log; never surfaced to the user. */
47+ cause: Schema.optional(Schema.Defect()),
MmacroscopeappBoton lines 45โ€“47Aug 16, 2026

BrowserImport/SafariCookies.ts:47

SafariCookieReadError carries only reason and cause, so a TCC denial and a corrupt jar both log without naming the file that was read. The path is available at every construction site (readSafariCookies receives cookiePath, and parseBinaryCookies can take it as a parameter), and the sibling ChromiumCookieReadError already carries cookieDatabasePath for exactly this reason. Consider adding a cookiePath: Schema.String structural attribute and deriving the message from it, e.g. Could not read Safari cookies at ${this.cookiePath}: ${this.reason}.

No longer relevant as of a8129c16f241155f34b7753fffe6c2f2d989201c

apps/desktop/src/preview/BrowserImport/Sources.tsline 393Outdated ยท a8129c1Open thread
โ€ฆโ€ฆ@@ -350,14 +380,17 @@ export const isSourceRunning = Effect.fn("BrowserImportSources.isSourceRunning")
390+ // profile, under three names across platforms (`lock` on macOS and Linux,
391+ // `.parentlock` beside it, `parent.lock` on Windows). Looking for Firefox's
392+ // at the root finds nothing and reports a running browser as importable.
393+ if (definition.engine === "safari") return false;
CcursorBoton line 393Aug 16, 2026

FDA denial reported as not installed

High Severity

Safariโ€™s userDataDirectory is the TCC-gated Cookies folder. isSourceInstalled treats any probe failureโ€”including PermissionDeniedโ€”as missing, so unavailableReason returns notInstalled and importCookies bails before readSafariCookies. Users without Full Disk Access never reach the needsFullDiskAccess path and see โ€œNot installedโ€ instead of the FDA remedy.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit a8129c16f241155f34b7753fffe6c2f2d989201c. Configure here.

apps/desktop/src/preview/BrowserImport/SafariCookies.tslines 100โ€“103Outdated ยท 7c940bbOpen thread
โ€ฆโ€ฆ@@ -0,0 +1,191 @@
100+ const cookieCount = page.readUInt32LE(4);
101+ if (COOKIE_PAGE_HEADER_SIZE + cookieCount * 4 > page.length) {
102+ throw new SafariCookieReadError({ reason: "readFailed" });
103+ }
MmacroscopeappBoton lines 100โ€“103Aug 17, 2026

๐ŸŸก Medium BrowserImport/SafariCookies.ts:100

A page with an invalid header signature is accepted and its cookies are imported instead of failing with readFailed. parseBinaryCookies reads cookieCount without checking the required big-endian 0x00000100 signature; validate that field before parsing the page body.

No longer relevant as of 675447ae50083288dda5f455f8b164b3d6902a5a

apps/desktop/src/preview/BrowserImport/SafariCookies.tslines 65โ€“68Outdated ยท 7c940bbOpen thread
โ€ฆโ€ฆ@@ -0,0 +1,191 @@
65+function readCString(buffer: Buffer, start: number): string {
66+ const end = buffer.indexOf(0, start);
67+ return buffer.toString("utf8", start, end === -1 ? buffer.length : end);
68+}
MmacroscopeappBoton lines 65โ€“68Aug 17, 2026

๐ŸŸก Medium BrowserImport/SafariCookies.ts:65

A truncated record with no NUL terminator is accepted and imported, with the affected field containing all remaining record bytes instead of producing the promised readFailed error. readCString falls back to buffer.length when indexOf returns -1; throw SafariCookieReadError when no terminator is found within the record.

No longer relevant as of 675447ae50083288dda5f455f8b164b3d6902a5a

apps/desktop/src/preview/BrowserImport/SafariCookies.tsline 34Outdated ยท 7c940bbOpen thread
โ€ฆโ€ฆ@@ -0,0 +1,191 @@
31+/** `u32 0x00000100`, `u32le cookieCount`, then one `u32le` offset per cookie. */
32+const COOKIE_PAGE_HEADER_SIZE = 12;
33+/** Through the `f64 creation` field; string bytes follow. */
34+const COOKIE_RECORD_HEADER_SIZE = 48;
CcursorBoton line 34Aug 17, 2026

Wrong cookie record header size

Low Severity

COOKIE_RECORD_HEADER_SIZE is 48, but the fixed record layout runs through the creation f64 at bytes 48โ€“55, so the header is 56 bytes. The comment and the test encoder both describe 56. The looser minimum lets truncated records past the malformation checks this parser otherwise enforces.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 7c940bb6d87c250a40b0cc522ac87ef5dae9bcb6. Configure here.

apps/desktop/src/preview/BrowserImport/SafariCookies.tsline 34Outdated ยท 05fc74fOpen thread
โ€ฆโ€ฆ@@ -0,0 +1,191 @@
31+/** `u32 0x00000100`, `u32le cookieCount`, then one `u32le` offset per cookie. */
32+const COOKIE_PAGE_HEADER_SIZE = 12;
33+/** Through the `f64 creation` field; string bytes follow. */
34+const COOKIE_RECORD_HEADER_SIZE = 48;
MmacroscopeappBoton line 34Aug 17, 2026

๐ŸŸก Medium BrowserImport/SafariCookies.ts:34

A truncated 48โ€“55-byte record can pass validation and be returned as a fabricated cookie instead of producing readFailed. COOKIE_RECORD_HEADER_SIZE must be 56, and string offsets must also be rejected when they are below 56, because the f64 creation field occupies offsets 48โ€“55.

๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/preview/BrowserImport/SafariCookies.ts around line 34:
2
3A truncated 48โ€“55-byte record can pass validation and be returned as a fabricated cookie instead of producing `readFailed`. `COOKIE_RECORD_HEADER_SIZE` must be `56`, and string offsets must also be rejected when they are below `56`, because the `f64 creation` field occupies offsets 48โ€“55.
apps/desktop/src/preview/BrowserImport/Sources.tsline 187Outdated ยท 6b977b6Open thread
โ€ฆโ€ฆ@@ -165,6 +165,26 @@ export const BROWSER_IMPORT_SOURCES: ReadonlyArray<BrowserImportSourceDefinition
184+ "Cookies",
185+ )
186+ : undefined,
187+ },
CcursorBoton line 187Aug 17, 2026

Safari profiles cookies ignored

Medium Severity

Safari has been modeled as a single jar under Library/Cookies, with listSourceProfiles always returning one synthetic . profile. Since Safari 17, additional profiles keep cookies under Library/WebKit/WebsiteDataStore/&lt;uuid&gt;/Cookies/. Those jars are never listed or read, so imports miss that profileโ€™s sessions, and a machine that only uses a named profile can look like Safari isnโ€™t installed.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 6b977b68e1c89e06021b3c504beb5d907685e937. Configure here.

Intentional for v1: only the container jar is read. Documented in docs/user/browser-import.md (423564cbb) so users with Safari profiles know what to expect.

apps/desktop/src/preview/BrowserImport/SafariCookies.tslines 146โ€“162Outdated ยท 2fffb54Open thread
MmacroscopeappBoton lines 146โ€“162Aug 17, 2026

๐ŸŸ  High BrowserImport/SafariCookies.ts:146

Host-only Safari cookies are imported with ImportedCookie.domain set to the bare host, so Electron treats them as domain cookies and widens them to subdomains (and rejects __Host- cookies). Set domain to undefined unless the source domain starts with ..

No longer relevant as of d7ea73ac1487159ef77bb8662246cf0700efdac3

apps/desktop/src/preview/BrowserImport/SafariCookies.tsline 155Outdated ยท 51247f4Open thread
โ€ฆโ€ฆ@@ -0,0 +1,191 @@
152+ // the modern browser default; claiming "none" would widen every
153+ // imported cookie's scope.
154+ sameSite: "lax",
155+ });
CcursorBoton line 155Aug 17, 2026

Host-only cookies widened

High Severity

Safari always sets domain on imported cookies, including host-only ones (no leading dot). Chromium and Firefox go through cookieScope, which leaves domain undefined so Electron keeps them host-only. Passing domain makes Electron treat them as domain cookies (re-adding the dot), which widens scope to subdomains and causes __Host- cookies to fail at session.cookies.set.

Reviewed by Cursor Bugbot for commit 51247f434e35aa6abc8b4410d17b50ae0bbf73b9. Configure here.

MmacroscopeappBotreviewed ยท 1 inline threadAug 17, 2026
Synced from GitHub

One finding: the Full Disk Access dialog's primary action is silently a no-op on desktop because the x-apple.systempreferences: scheme is blocked by the main-process external-URL allowlist.

Posted via Macroscope โ€” UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxlines 921โ€“927Outdated ยท 675447aOpen thread
โ€ฆโ€ฆ@@ -882,6 +901,36 @@ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) {
924+ void readLocalApi()
925+ ?.shell.openExternal(FULL_DISK_ACCESS_SETTINGS_URL)
926+ .catch(() => undefined);
927+ }}
MmacroscopeappBoton lines 921โ€“927Aug 17, 2026

The dialog's primary action can't open anything today: ElectronShell.parseSafeExternalUrl (apps/desktop/src/electron/ElectronShell.ts) only allows http:, https: and the editor remote schemes, so x-apple.systempreferences: is rejected, desktopBridge.openExternal resolves false, localApi.shell.openExternal throws "Unable to open link.", and .catch(() => undefined) swallows it. AlertDialogClose still dismisses the dialog, so the user is left with a button that closes the sheet and does nothing โ€” the one recovery path this dialog exists to offer.

Smallest fix: add "x-apple.systempreferences:" to SAFE_EXTERNAL_PROTOCOLS (macOS-only is fine) and, so a future block isn't invisible, replace the bare .catch(() => undefined) with the existing failure toast telling the user to open System Settings โ†’ Privacy & Security โ†’ Full Disk Access manually.

Posted via Macroscope โ€” UI Consistency

MmacroscopeappBotreviewedAug 17, 2026
Synced from GitHub

UI consistency review: one finding on the new Full Disk Access dialog in apps/web/src/components/settings/IntegrationsSettings.tsx.

Posted via Macroscope โ€” UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxline 959Outdated ยท 47e56cdOpen thread
โ€ฆโ€ฆ@@ -882,6 +913,50 @@ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) {
956+ </Button>
957+ </AlertDialogFooter>
958+ </AlertDialogPopup>
959+ </AlertDialog>
CcursorBoton line 959Aug 17, 2026

FDA retry needs restart

Medium Severity

The Full Disk Access dialog keeps the failed import and offers โ€œI've turned it onโ€ to retry immediately. macOS Full Disk Access only applies after the app quits and relaunches, which also clears this in-memory pending state. Immediate retries keep failing with needsFullDiskAccess, and a real relaunch drops the held import so the button cannot complete the flow.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 47e56cdf71a05215bc9906432d81bb772477b504. Configure here.

MmacroscopeappBotreviewed ยท 1 inline threadAug 17, 2026
Synced from GitHub

One finding: the Full Disk Access step's "Open System Settings" button cannot open anything today, and the failure is swallowed. Details inline.

Posted via Macroscope โ€” UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxlines 849โ€“853Outdated ยท 1a6149bOpen thread
โ€ฆโ€ฆ@@ -836,6 +846,11 @@ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) {
850+ void readLocalApi()
851+ ?.shell.openExternal(FULL_DISK_ACCESS_SETTINGS_URL)
852+ .catch(() => undefined)
853+ }
MmacroscopeappBoton lines 849โ€“853Aug 17, 2026

This still can't open anything, and after the move into BrowserImportWizard the button is now a complete no-op (it no longer dismisses the dialog either). ElectronShell.parseSafeExternalUrl (apps/desktop/src/electron/ElectronShell.ts) allows only http:, https: and the remote editor schemes, so x-apple.systempreferences: is rejected, desktopBridge.openExternal resolves false, localApi.shell.openExternal throws "Unable to open link.", and .catch(() => undefined) discards it. The user clicks the step's action and nothing happens or is said.

Smallest fix: allow the x-apple.systempreferences: scheme in SAFE_EXTERNAL_PROTOCOLS (darwin only), and surface a failure here instead of swallowing it โ€” e.g. a toast in the catch, as ThreadTerminalDrawer does โ€” so the step degrades to readable instructions rather than a dead control.

Posted via Macroscope โ€” UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxline 528Outdated ยท 1a6149bOpen thread
โ€ฆโ€ฆ@@ -517,6 +519,14 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
525+ * later.
526+ */
527+const FULL_DISK_ACCESS_SETTINGS_URL =
528+ "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFilesAccess";
CcursorBoton line 528Aug 17, 2026

Wrong Full Disk Access URL

Medium Severity

FULL_DISK_ACCESS_SETTINGS_URL uses the query fragment Privacy_AllFilesAccess. Documented macOS deep links for Full Disk Access use Privacy_AllFiles. Even after the scheme is allowlisted, this URL may open the wrong pane or fail to land on Full Disk Access.

Reviewed by Cursor Bugbot for commit 1a6149b764f4000a565e3f5e0abca29797a769de. Configure here.

apps/web/src/components/settings/IntegrationsSettings.tsxline 528Outdated ยท 1a6149bOpen thread
โ€ฆโ€ฆ@@ -517,6 +519,14 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
525+ * later.
526+ */
527+const FULL_DISK_ACCESS_SETTINGS_URL =
528+ "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFilesAccess";
CcursorBoton line 528Aug 17, 2026

Settings deep link blocked silently

High Severity

Open System Settings calls shell.openExternal with an x-apple.systempreferences: URL. Desktop openExternal only allows http/https and editor schemes, so the call returns false, readLocalApi throws, and the handlerโ€™s .catch swallows it โ€” the button does nothing.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 1a6149b764f4000a565e3f5e0abca29797a769de. Configure here.

MmacroscopeappBotreviewed ยท 1 inline threadAug 17, 2026
Synced from GitHub

One follow-up on the Safari cookie error context; everything else in the Safari path (namespace imports, Schema.TaggedErrorClass with structural attributes and a message derived from them, Effect.catchTags in BrowserImport.ts, Effect.fn construction) matches the conventions used by the sibling Chromium/Firefox readers.

Posted via Macroscope โ€” Effect Service Conventions

apps/desktop/src/preview/BrowserImport/SafariCookies.tslines 197โ€“206Outdated ยท dbaf712Open thread
โ€ฆโ€ฆ@@ -0,0 +1,208 @@
203+ reason: "readFailed",
204+ cookieDatabasePath: cookiePath,
205+ cause,
206+ }),
MmacroscopeappBoton lines 197โ€“206Aug 17, 2026

BrowserImport/SafariCookies.ts:206

Parse failures still lose the path they occurred on: parseBinaryCookies throws without cookieDatabasePath, and the pass-through branch returns that error unchanged even though cookiePath is in hand here, so a corrupt jar logs Could not read Safari cookies: readFailed. with no file named. Consider threading the path into the parser (e.g. parseBinaryCookies(buffer, cookiePath) used at each throw new SafariCookieReadError({ reason: "readFailed", cookieDatabasePath })), which also lets cookieDatabasePath become required like ChromiumCookieReadError's.

No longer relevant as of 46430d0e1804d8d4350ba986269dbe277cb17691

apps/desktop/src/preview/BrowserImport/Sources.tsline 79Outdated ยท d7ea73aOpen thread
โ€ฆโ€ฆ@@ -69,51 +69,36 @@ const chromiumSource = (input: {
8477 platforms: [
8578 "darwin" as NodeJS.Platform,
86- ...(input.windowsSegments || input.windowsRoamingSegments ? ["win32" as NodeJS.Platform] : []),
8779 ...(input.linuxSegments ? ["linux" as NodeJS.Platform] : []),
MmacroscopeappBoton line 79Aug 18, 2026

๐ŸŸ  High BrowserImport/Sources.ts:79

Chromium sources are now reported as unsupported on Windows, so BrowserImport rejects them before the reader can import still-readable DPAPI v10 cookies from databases that also contain unreadable App-Bound v20 records. Restore Windows platform/path definitions for Chromium sources so the supported partial-import path remains reachable.

๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/preview/BrowserImport/Sources.ts around line 79:
2
3Chromium sources are now reported as unsupported on Windows, so `BrowserImport` rejects them before the reader can import still-readable DPAPI `v10` cookies from databases that also contain unreadable App-Bound `v20` records. Restore Windows platform/path definitions for Chromium sources so the supported partial-import path remains reachable.

Reviewed against the current scope and the Windows-tested contribution #7323. Windows Chromium import was deliberately removed at the maintainerโ€™s request; restoring the retired DPAPI path is not a correction for this stack. Firefox remains supported on Windows. Resolving this as an intentional unsupported capability, not an outstanding regression.

apps/desktop/src/preview/BrowserImport/Sources.test.tsline 365Outdated ยท d7ea73aOpen thread
โ€ฆโ€ฆ@@ -232,35 +343,47 @@ describe("isSourceRunning for Firefox", () => {
362+ // persists after the process exits. The file is only locked while
363+ // Firefox is running; the old stat-based check always found it.
364+ yield* fileSystem.writeFileString(`${profile}/parent.lock`, "");
365+ assert.isFalse(yield* isSourceRunning(firefox, context));
CcursorBoton line 365Aug 18, 2026

Windows lock test never runs

Medium Severity

The new Windows parent.lock test only supplies HOME, but Firefox on win32 resolves its user-data directory from APPDATA. With that missing, userDataDirectory is undefined and isSourceRunning returns false before isLockHeld runs, so the assertion passes without covering the stale-lock regression it describes.

Reviewed by Cursor Bugbot for commit d7ea73ac1487159ef77bb8662246cf0700efdac3. Configure here.

apps/desktop/src/preview/BrowserImport/Sources.tslines 312โ€“312Outdated ยท 1453d76Open thread
โ€ฆโ€ฆ@@ -253,6 +292,31 @@ const entryExists = Effect.fnUntraced(function* (path: string) {
309+ return yield* fileSystem.open(lockPath, { flag: "r+" }).pipe(
310+ Effect.as(false),
311+ Effect.catchIf(
312+ (error) => error.reason._tag === "Busy" || error.reason._tag === "PermissionDenied",
MmacroscopeappBoton lines 312โ€“312Aug 28, 2026

๐ŸŸ  High BrowserImport/Sources.ts:312

isLockHeld reports Firefox as running for a stale read-only parent.lock or a profile directory without write access, even when no Firefox process holds the lock. The r+ probe maps those EACCES cases to PermissionDenied, and treating that error like Busy makes isSourceRunning return true and blocks cookie imports; only an actual sharing conflict should indicate an active lock.

diff
1- (error) => error.reason._tag === "Busy" || error.reason._tag === "PermissionDenied",
2+ (error) => error.reason._tag === "Busy",
๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/preview/BrowserImport/Sources.ts around line 312:
2
3`isLockHeld` reports Firefox as running for a stale read-only `parent.lock` or a profile directory without write access, even when no Firefox process holds the lock. The `r+` probe maps those `EACCES` cases to `PermissionDenied`, and treating that error like `Busy` makes `isSourceRunning` return `true` and blocks cookie imports; only an actual sharing conflict should indicate an active lock.
apps/desktop/src/electron/ElectronShell.tsline 25Outdated ยท 46430d0Open thread
โ€ฆโ€ฆ@@ -1,11 +1,29 @@
22+const SYSTEM_SETTINGS_URLS: Record<SystemSettingsPane, string> = {
23+ "full-disk-access":
24+ "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFilesAccess",
25+};
CcursorBoton line 25Aug 28, 2026

FDA deep link uses wrong anchor

Medium Severity

The Full Disk Access deep link pairs the Ventura PrivacySecurity.extension pane with query Privacy_AllFilesAccess, carried over from the legacy preference.security URL. The documented anchor for this pane is Privacy_AllFiles. An unrecognized anchor typically opens Privacy & Security without jumping to Full Disk Access, so Open System Settings can land users on the wrong screen.

Reviewed by Cursor Bugbot for commit 46430d0e1804d8d4350ba986269dbe277cb17691. Configure here.

MmacroscopeappBotreviewed ยท 1 inline threadAug 29, 2026
Synced from GitHub

One finding: the new needsFullDiskAccess reason is handled on the import-failure path but not on the wizard's entry path, so the same condition reaches two different screens.

Posted via Macroscope โ€” UI Consistency

apps/web/src/components/settings/browserImportWizard.logic.tslines 62โ€“62Outdated ยท e6497eeOpen thread
โ€ฆโ€ฆ@@ -58,6 +59,7 @@ export function outcomeToStep(outcome: ImportOutcome): WizardStep {
5859 // other failure surfaces on the blocked screen, which offers a retry when
5960 // one could help.
6061 if (outcome.reason === "browserRunning") return { step: "quit" };
62+ if (outcome.reason === "needsFullDiskAccess") return { step: "fullDiskAccess" };
MmacroscopeappBoton lines 62โ€“62Aug 29, 2026

needsFullDiskAccess is now also a BrowserImportUnavailableReason, but only outcomeToStep routes it to the new screen. initialWizardStep still falls through to { step: "blocked", reason: source.unavailable }, and isRetryableReason returns false for it โ€” so a source listed as unavailable for this reason opens on a dead-end screen whose copy says "โ€ฆthen retry" with no retry and no "Open System Settings" button, while the identical condition discovered at import time gets the recoverable step. It's latent today (the desktop only reports unsupportedPlatform/notInstalled/browserRunning pre-flight), but the two entry points should agree, the same way browserRunning maps to quit in both.

diff
1 if (source.unavailable === "browserRunning") return { step: "quit" };
2+ if (source.unavailable === "needsFullDiskAccess") return { step: "fullDiskAccess" };

Posted via Macroscope โ€” UI Consistency

Fixed in the updated #7262. Initial Full Disk Access requirements route into the permission step and return to configuration after refresh; permission failure during a selected import preserves its retry path. Opening settings failures show actionable feedback rather than disappearing. Focused state-transition tests pass.

MmacroscopeappBotreviewed ยท 2 inline threadsAug 29, 2026
Synced from GitHub

Two findings in the changed web UI code, both about the new Full Disk Access flow.

Posted via Macroscope โ€” UI Consistency

apps/web/src/localApi.tsline 36Outdated ยท b99d5ddOpen thread
โ€ฆโ€ฆ@@ -30,6 +30,15 @@ function createBrowserLocalApi(): LocalApi {
33+ // Only the desktop shell can reach the OS; the web build (and older
34+ // desktop shells that predate this method) have nothing to open.
35+ openSystemSettings: async (pane) => {
36+ if (!window.desktopBridge?.openSystemSettings) return;
MmacroscopeappBoton line 36Aug 29, 2026

Resolving instead of throwing here makes the "Open System Settings" button a silent no-op on any shell whose preload predates openSystemSettings: the promise fulfils, so the .catch in IntegrationsSettings.tsx never fires and the "Could not open System Settings" toast never shows. The other guard there doesn't cover it either โ€” readLocalApi() only returns undefined when window is undefined, so the !localApi branch is unreachable in the renderer.

Throwing keeps a single feedback path (the caller's toast telling the user where to click), matching openExternal above.

suggestion
1 if (!window.desktopBridge?.openSystemSettings) {
2 throw new Error("Unable to open System Settings.");
3 }

Posted via Macroscope โ€” UI Consistency

Fixed in 0369f309c0 on #7262. An unavailable System Settings bridge rejects with the established error, reaching the caller toast. A focused missing-bridge test covers this path.

apps/web/src/components/settings/BrowserImportWizard.tsxlines 115โ€“125Outdated ยท b99d5ddOpen thread
โ€ฆโ€ฆ@@ -109,11 +112,30 @@ export function BrowserImportWizard({
122+ setStep(refreshedSourceStep(refreshed));
123+ })
124+ .catch(() => setStep({ step: "blocked", reason: "readFailed" }));
125+ };
MmacroscopeappBoton lines 115โ€“125Aug 29, 2026

recheckFullDiskAccess runs an async round-trip with no step change, unlike its sibling recheckAfterQuit which sets { step: "checking" } first. Pressing "I've turned it on" leaves the same screen mounted with no in-flight state, and when access is still denied refreshedSourceStep returns fullDiskAccess again โ€” so the button looks dead in exactly the case the user is most likely to hit (toggling the checkbox requires relaunching the app on macOS).

Smallest fix: set an in-flight step before awaiting, as the quit path does, and give CheckingStep neutral copy (or a description prop) since "Checking whether the browser has closed." is wrong for this screen; a distinct message when the refresh still reports needsFullDiskAccess would also make the outcome visible.

Also, this path resets the source profile to refreshed.profiles[0] rather than going through refreshedSourceProfileDirectory, which the quit path uses to preserve the user's selection when the refreshed source still lists it.

Posted via Macroscope โ€” UI Consistency

Fixed in cfad01d520 on #7262. Full Disk Access refresh enters an explicit checking state with accurate copy, preserves the selected source profile through the shared helper, and shows a still-required message after denial. Initial permission and retry-import paths remain intact.

apps/desktop/src/preview/BrowserImport/BrowserImport.tsfileOutdated ยท b99d5ddOpen thread
MmacroscopeappBoton fileAug 29, 2026

๐ŸŸก Medium

https://github.com/pingdotgg/t3code/blob/b99d5dd2476b927e86a541e0bd6e17f712486780/apps/desktop/src/preview/BrowserImport/BrowserImport.ts#L193-L199

Safari imports emit Reading browser cookie key from the keychain even though Safari never obtains a key, misleading logs and keychain-prompt diagnostics on every Safari import. Emit this log only for Chromium imports.

diff
1- // macOS attributes the Keychain prompt and the resulting ACL grant to the
2- // executable that asks, so record which one that was โ€” in a packaged build
3- // it is the signed app, in dev whatever binary hosts the main process.
4- yield* Effect.logInfo("Reading browser cookie key from the keychain", {
5- sourceId: definition.id,
6- executablePath,
7- });
8+ if (definition.engine === "chromium") {
9+ // macOS attributes the Keychain prompt and the resulting ACL grant to the
10+ // executable that asks, so record which one that was โ€” in a packaged build
11+ // it is the signed app, in dev whatever binary hosts the main process.
12+ yield* Effect.logInfo("Reading browser cookie key from the keychain", {
13+ sourceId: definition.id,
14+ executablePath,
15+ });
16+ }
๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/preview/BrowserImport/BrowserImport.ts around lines 193-199:
2
3Safari imports emit `Reading browser cookie key from the keychain` even though Safari never obtains a key, misleading logs and keychain-prompt diagnostics on every Safari import. Emit this log only for Chromium imports.

Fixed in b056322b87 on #7262. Keychain-read diagnostics now run only for macOS Chromium imports; Safari, Firefox, and Linux no longer emit the misleading message.

MmacroscopeappBotreviewed ยท 1 inline threadAug 29, 2026
Synced from GitHub

One finding on the new Full Disk Access screen: the "still required" feedback only reaches one of the two onGranted paths.

Posted via Macroscope โ€” UI Consistency

apps/web/src/components/settings/BrowserImportWizard.tsxline 141Outdated ยท b056322Open thread
โ€ฆโ€ฆ@@ -109,15 +113,38 @@ export function BrowserImportWizard({
138+ source={source}
139+ onCancel={onClose}
140+ onOpenSettings={onOpenFullDiskAccessSettings}
141+ onGranted={step.resume === "import" ? runImport : recheckFullDiskAccess}
MmacroscopeappBoton line 141Aug 29, 2026

The two onGranted branches give different feedback for the same user action. recheckFullDiskAccess routes through fullDiskAccessRecheckStep, which sets checked: true and renders the "Full Disk Access is still required" panel; the resume === "import" branch calls runImport, whose failure goes through outcomeToStep and returns { step: "fullDiskAccess", resume: "import" } with no checked. So on the import-retry path the user presses "I've turned it on", sees a brief spinner, and lands back on an unchanged screen with nothing saying access is still denied โ€” which is the common case, since granting Full Disk Access on macOS only takes effect after the app relaunches.

Smallest fix: mark the returned step as checked when the retry came from this screen, e.g. give runImport an afterAccessGrant flag that maps a fullDiskAccess outcome to { ...next, checked: true }, and call () => runImport(true) here. (Alternatively, add checked: true to the needsFullDiskAccess branch of outcomeToStep, which also shows the panel on first arrival.)

Posted via Macroscope โ€” UI Consistency

Fixed in 4799270451 on #7262. An import attempt denied Full Disk Access now returns checked:true, showing the same still-required feedback as a permission refresh. The state-transition test covers it.

apps/desktop/src/preview/BrowserImport/Sources.tsline 413Outdated ยท 5c9c06fOpen thread
โ€ฆโ€ฆ@@ -343,8 +408,11 @@ const countProfileCookies = Effect.fnUntraced(function* (
347- const databasePath = cookieDatabasePath(definition, context, directory);
411+): Effect.fn.Return<number | undefined, never, FileSystem.FileSystem> {
412+ const candidates = cookieDatabaseCandidatePaths(definition, context, directory);
413+ const databasePath = yield* Effect.forEach(candidates, (candidate) =>
MmacroscopeappBoton line 413Sep 1, 2026

๐ŸŸ  High BrowserImport/Sources.ts:413

When Network/Cookies is a directory and the valid legacy Cookies database exists, countProfileCookies selects the directory first, the SQLite query fails, and the legacy candidate is never tried. The importer uses the same candidate selection and therefore also fails instead of importing the profile; filter candidates to regular files in both paths.

๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/preview/BrowserImport/Sources.ts around line 413:
2
3When `Network/Cookies` is a directory and the valid legacy `Cookies` database exists, `countProfileCookies` selects the directory first, the SQLite query fails, and the legacy candidate is never tried. The importer uses the same candidate selection and therefore also fails instead of importing the profile; filter candidates to regular files in both paths.
apps/desktop/src/preview/BrowserImport/Sources.tslines 447โ€“447Outdated ยท 8dfa569Open thread
โ€ฆโ€ฆ@@ -381,28 +423,50 @@ export const listSourceProfiles = Effect.fn("BrowserImportSources.listSourceProf
444+ (candidate) => databaseFileExists(candidate),
445+ ).pipe(Effect.map((results) => (results.some(Boolean) ? profile : undefined))),
446+ );
447+ return yield* withCookieCounts(
MmacroscopeappBoton lines 447โ€“447Sep 2, 2026

๐ŸŸ  High BrowserImport/Sources.ts:447

When profiles.ini lists only profiles without cookies.sqlite, listSourceProfiles returns an empty list without scanning for other profiles, so isSourceInstalled returns false and hides cookies in an unlisted profile. Continue to the fallback directory scan when found is empty, rather than returning whenever declared is non-empty.

diff
1- return yield* withCookieCounts(
2+ if (found.some((profile) => profile !== undefined)) return yield* withCookieCounts(
๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/preview/BrowserImport/Sources.ts around line 447:
2
3When `profiles.ini` lists only profiles without `cookies.sqlite`, `listSourceProfiles` returns an empty list without scanning for other profiles, so `isSourceInstalled` returns `false` and hides cookies in an unlisted profile. Continue to the fallback directory scan when `found` is empty, rather than returning whenever `declared` is non-empty.

Fixed in 78c57bad1d. When none of the declared profiles has a cookie database, listing now falls through to the Profiles directory scan instead of returning empty, so cookies in an undeclared profile are found and isSourceInstalled stays true. Regression test covers a profiles.ini that names only a never-launched profile.

apps/desktop/src/preview/BrowserImport/SafariCookies.tsline 202Outdated ยท 78c57baOpen thread
โ€ฆโ€ฆ@@ -0,0 +1,216 @@
199+ // The parser throws on a malformed jar; catch it here so callers see a typed
200+ // failure rather than a defect.
201+ return yield* Effect.try({
202+ try: () => parseBinaryCookies(Buffer.from(contents)),
MmacroscopeappBoton line 202Sep 2, 2026

๐ŸŸ  High BrowserImport/SafariCookies.ts:202

readSafariCookies successfully imports a truncated/partial jar while silently omitting cookies from complete pages left as trailing bytes. parseBinaryCookies validates each declared page but never checks that the final pageStart equals buffer.length; add that validation so trailing data causes readFailed instead of a partial import.

๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/preview/BrowserImport/SafariCookies.ts around line 202:
2
3`readSafariCookies` successfully imports a truncated/partial jar while silently omitting cookies from complete pages left as trailing bytes. `parseBinaryCookies` validates each declared page but never checks that the final `pageStart` equals `buffer.length`; add that validation so trailing data causes `readFailed` instead of a partial import.

Fixed in 453644b15e. The parser now requires the bytes after the declared pages to be exactly what Safari writes โ€” nothing, the 8-byte checksum, or checksum + u32 length + that many property-list bytes โ€” and refuses anything else, so a jar with undeclared trailing pages is readFailed rather than a partial import. Tests cover the valid trailers and both the extra-page and bad-length cases.

apps/desktop/src/preview/BrowserImport/SafariCookies.tslines 176โ€“178Outdated ยท 78c57baOpen thread
โ€ฆโ€ฆ@@ -0,0 +1,216 @@
175+export const isPermissionDenied = (error: PlatformError.PlatformError): boolean => {
176+ if (error.reason._tag === "PermissionDenied") return true;
177+ const code = (error.reason as { cause?: { code?: unknown } }).cause?.code;
178+ return code === "EPERM" || code === "EACCES";
MmacroscopeappBoton lines 176โ€“178Sep 2, 2026

๐ŸŸ  High BrowserImport/SafariCookies.ts:176

isPermissionDenied reports ordinary EACCES failures as needsFullDiskAccess, so jars blocked by POSIX permissions or ACLs send users to a grant that cannot fix the failure. Since the Safari TCC path is identified by EPERM, only classify that errno as needsFullDiskAccess and let other permission errors remain readFailed.

suggestion
1 const code = (error.reason as { cause?: { code?: unknown } }).cause?.code;
2 return code === "EPERM";
๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/preview/BrowserImport/SafariCookies.ts around lines 176-178:
2
3`isPermissionDenied` reports ordinary `EACCES` failures as `needsFullDiskAccess`, so jars blocked by POSIX permissions or ACLs send users to a grant that cannot fix the failure. Since the Safari TCC path is identified by `EPERM`, only classify that errno as `needsFullDiskAccess` and let other permission errors remain `readFailed`.
apps/desktop/src/preview/BrowserImport/SafariCookies.tslines 105โ€“114Outdated ยท ffc45f7Open thread
โ€ฆโ€ฆ@@ -0,0 +1,236 @@
111+ // it into the following record's bytes.
112+ const recordSize = page.readUInt32LE(cookieStart);
113+ if (recordSize < COOKIE_RECORD_HEADER_SIZE || cookieStart + recordSize > page.length) {
114+ throw new SafariCookieReadError({ reason: "readFailed" });
MmacroscopeappBoton lines 105โ€“114Sep 2, 2026

๐ŸŸก Medium BrowserImport/SafariCookies.ts:105

Malformed pages can place cookieStart inside the page header/offset table or overlap a previously parsed record, allowing those bytes to be interpreted as a fabricated cookie. The check at line 107 only verifies page bounds; require cookieStart to start after the complete offset table and reject overlapping record ranges before parsing the record.

diff
1 for (let index = 0; index < cookieCount; index += 1) {
2+ const cookieTableEnd = COOKIE_PAGE_HEADER_SIZE + cookieCount * 4;
3+ const recordRanges: Array<readonly [number, number]> = [];
4 const cookieStart = page.readUInt32LE(8 + index * 4);
5- if (cookieStart + COOKIE_RECORD_HEADER_SIZE > page.length) {
6+ if (cookieStart < cookieTableEnd || cookieStart + COOKIE_RECORD_HEADER_SIZE > page.length) {
7 throw new SafariCookieReadError({ reason: "readFailed" });
8 }
9@@
10- if (recordSize < COOKIE_RECORD_HEADER_SIZE || cookieStart + recordSize > page.length) {
11+ if (
12+ recordSize < COOKIE_RECORD_HEADER_SIZE ||
13+ cookieStart + recordSize > page.length ||
14+ recordRanges.some(([start, end]) => cookieStart < end && cookieStart + recordSize > start)
15+ ) {
16 throw new SafariCookieReadError({ reason: "readFailed" });
17 }
18+ recordRanges.push([cookieStart, cookieStart + recordSize]);
๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/preview/BrowserImport/SafariCookies.ts around lines 105-114:
2
3Malformed pages can place `cookieStart` inside the page header/offset table or overlap a previously parsed record, allowing those bytes to be interpreted as a fabricated cookie. The check at line 107 only verifies page bounds; require `cookieStart` to start after the complete offset table and reject overlapping record ranges before parsing the record.
apps/desktop/src/preview/BrowserImport/SafariCookies.tslines 102โ€“102Outdated ยท 6918217Open thread
โ€ฆโ€ฆ@@ -0,0 +1,247 @@
99+
100+ // Page bodies switch to little-endian after the big-endian header.
101+ const cookieCount = page.readUInt32LE(4);
102+ const offsetTableEnd = COOKIE_PAGE_HEADER_SIZE + cookieCount * 4;
MmacroscopeappBoton lines 102โ€“102Sep 2, 2026

๐ŸŸ  High BrowserImport/SafariCookies.ts:102

parseBinaryCookies rejects valid Safari pages, so ordinary one-cookie jars fail to import. offsetTableEnd should start at byte 8, but the current 12 + cookieCount * 4 makes a record offset of 12 fail the cookieStart < offsetTableEnd check; use 8 + cookieCount * 4 instead.

suggestion
1 const offsetTableEnd = 8 + cookieCount * 4;
๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/preview/BrowserImport/SafariCookies.ts around line 102:
2
3`parseBinaryCookies` rejects valid Safari pages, so ordinary one-cookie jars fail to import. `offsetTableEnd` should start at byte 8, but the current `12 + cookieCount * 4` makes a record offset of 12 fail the `cookieStart < offsetTableEnd` check; use `8 + cookieCount * 4` instead.
CcursorBotreviewedSep 2, 2026
Synced from GitHub

Stale Bugbot comment from a previous run.

Synced from GitHub

Macroscope skipped reviewing this pull request. Per-PR cost limit exceeded (workspace setting).

Reviews on this PR have cost $48.44 so far. This review would add an estimated $1.85, bringing the total to $50.29 โ€” above your per-PR limit of $50.00.

Tip

To get this pull request reviewed, you can:

  1. Comment @macroscope-app on this PR to request a manual review (monthly spend limits still apply).
  2. Exclude large or generated files from review by adding a pattern to your .macroscope/ignore.md โ€” note that creating this file replaces Macroscope's built-in default ignores rather than extending them.
  3. Raise your cost limit in your workspace billing settings.

Turn off this reminder going forward

Added 18 commits

  • feat(desktop): import cookies from Safari2b608c0
  • fix(web): explain denied Safari import retries0945140
  • fix(desktop): open the Full Disk Access settings pane0dcadc3
  • refactor(desktop): name the jar a Safari read failed on23d6c1a
  • fix(desktop): open System Settings from the Full Disk Access step273b764
  • fix(desktop): retain Safari cookie path on parse errors289c96b
  • fix(desktop): report Safari's TCC denial as needing Full Disk Access2b7849a
  • fix(desktop): tell Safari's Full Disk Access apart at listing time35f4dcd
  • fix(web): reject unavailable System Settings links3833440
  • fix(desktop): reject a malformed Safari jar instead of importing part of it39ef011
  • fix(desktop): reject Safari records that overlap the page table or each other3a8fcf7
  • fix(web): report Safari access rechecks46f6a53
  • fix(desktop): bracket Safari IPv6 hosts and skip directory candidates658ff9e
  • test(web): prioritize Safari access recovery94f66f4
  • fix(desktop): validate Safari cookie record headerscda6152
  • fix(web): recover initial Safari accessdc3c406
  • feat(web): add the Full Disk Access step to the import wizarde3ab027
  • fix(desktop): refuse a Safari jar its page table doesn't describe, and route only TCC to Full Disk Accessf979d6f

Added 2 commits

  • fix(desktop): keep host-only Safari cookies host-only9611ffb
  • fix(web): tell the user to relaunch when Full Disk Access still reads denied423564c
Synced from GitHub

macOS preview

The preview download was removed because this PR closed or the preview label was removed.

Added 1 commit

  • feat(desktop): discover Safari profile cookie storesc6cb164
CcursorBotreviewed ยท 1 inline threadSep 5, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before autofix could start.

Reviewed by Cursor Bugbot for commit c6cb164bc007a02da724bb446fa2d1efceb1b8b5. Configure here.

โ€ฆโ€ฆ@@ -386,6 +410,64 @@ const withCookieCounts = (
466+ }
467+ }
468+ return profiles;
469+});
CcursorBoton line 469Sep 5, 2026

FDA check skips named Safari jars

Medium Severity

listSafariProfiles can mark Safari installed from a named-profile jar, but the listing-time Full Disk Access probe still only opens the default "." jar. If that default jar is missing, TCC denial is not reported up front, so Safari looks ready or notInstalled instead of opening the permission step.

Reviewed by Cursor Bugbot for commit c6cb164bc007a02da724bb446fa2d1efceb1b8b5. Configure here.