Skip to content

feat(desktop): import cookies from Safari - #7262

Open
juliusmarminge wants to merge 6 commits into
browser-import-linux-windowsfrom
browser-import-safari
Open

feat(desktop): import cookies from Safari#7262
juliusmarminge wants to merge 6 commits into
browser-import-linux-windowsfrom
browser-import-safari

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Stacked on #7261 — review that first; this PR's base is browser-import-linux-windows.

Adds Safari, completing the source list.

Safari does not encrypt its cookies. It stores them in a proprietary Cookies.binarycookies file whose protection is TCC rather than cryptography: the file sits inside the app container, readable only by apps with Full Disk Access. So the gate is a permission you grant in System Settings, and a denial is reported as exactly that.

Two details the format forces

Timestamps count from 2001-01-01, not the UNIX epoch. Without rebasing, every cookie imports as ~31 years expired.

The format predates SameSite and carries no equivalent field. Imported cookies are marked Lax, the modern browser default — claiming none would widen the scope of every cookie Safari ever set.

Safari also keeps one jar for the whole app rather than per-profile, so it exposes a single implicit profile, and it has no observable lock file since the jar is written atomically.

Testing

The parser is covered by tests that build the binary format byte for byte, including a multi-page file — Safari pages its jar, and a single-page reader would silently return only the first slice.

That coverage matters because the real file cannot be read on this machine without the very permission the feature asks for. The TCC path itself was verified against the live file: it denies with EPERM and surfaces needsFullDiskAccess with an actionable remedy rather than a generic failure.

Not verified against a real Cookies.binarycookies. The layout follows the documented format and round-trips through a byte-accurate fixture, but no genuine Safari jar has been parsed — worth one read with Full Disk Access granted before relying on it.

Note

Add Safari cookie import support to the browser import wizard

  • Adds a binary parser for Safari's Cookies.binarycookies format in SafariCookies.ts, including bounds checking, Apple-to-Unix epoch conversion, and structured SafariCookieReadError with needsFullDiskAccess or readFailed reasons.
  • Registers Safari as a browser source on macOS in Sources.ts, pointing to ~/Library/Containers/com.apple.Safari/Data/Library/Cookies/Cookies.binarycookies, with a single pseudo-profile and no running-browser lock check.
  • Adds a FullDiskAccessStep to the import wizard that guides users to grant Full Disk Access in macOS System Settings and retry the import when needsFullDiskAccess is returned.
  • Extends contracts in browserImport.ts to accept "safari" as a source ID and "needsFullDiskAccess" as an unavailable reason with user-facing copy.
  • Risk: macOS TCC permission detection relies on EPERM/EACCES error codes and PlatformError tagging; other OS-level read failures fall back to the generic readFailed reason.

Macroscope summarized dbaf712.


Note

Medium Risk
Imports real session cookies from a protected system path and relies on a reverse-engineered binary format not yet validated against a live Safari jar; mis-parsing or wrong metadata could silently skip or mis-set cookies.

Overview
Safari cookie import is wired end-to-end on macOS: a new safari engine reads Cookies.binarycookies from Safari’s container, maps Apple-epoch timestamps and flags into the shared ImportedCookie shape (defaulting SameSite to lax), and plugs into BrowserImport alongside Chromium and Firefox.

Desktop plumbing extends Sources (single implicit profile, no lock-file “running” check, Cookies.binarycookies path) and treats TCC EPERM as needsFullDiskAccess instead of a generic read failure. The parser rejects malformed or truncated jars so partial imports cannot look successful.

UI and contracts add safari and needsFullDiskAccess, route that reason to a fullDiskAccess wizard step with Open System Settings, and export importFailureReason for tests. Coverage includes byte-built fixtures (multi-page jars) and permission-denial behavior.

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

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

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: fce47684-5f49-43a0-954b-7eafc617e370

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.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 16, 2026
Comment thread apps/desktop/src/preview/BrowserImport/SafariCookies.ts
@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ No successful main baseline artifact is available yet. This run establishes the initial measurement.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 12.6 KiB 15.1 KiB
Codex Thread snapshot wire 6.3 KiB 7.3 KiB
Codex Live turn WebSocket wire 6.3 KiB 7.8 KiB
Codex Live turn WebSocket decoded 51.8 KiB 66.4 KiB
Codex Live turn messages 16 21
Claude Total thread wire 12.6 KiB 15.1 KiB
Claude Thread snapshot wire 6.3 KiB 7.3 KiB
Claude Live turn WebSocket wire 6.3 KiB 7.8 KiB
Claude Live turn WebSocket decoded 52.7 KiB 66.4 KiB
Claude Live turn messages 16 21

Baseline: unavailable · PR result: 51247f4 · 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: 99.9 KiB
  • Claude decoded thread snapshot: 100.6 KiB

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

@github-actions github-actions Bot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026
Comment thread apps/desktop/src/preview/BrowserImport/SafariCookies.ts Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 22:18
Comment thread apps/desktop/src/preview/BrowserImport/SafariCookies.ts
Comment thread apps/desktop/src/preview/BrowserImport/SafariCookies.ts
@macroscopeapp

macroscopeapp Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

2 blocking correctness issues found. New feature adding Safari cookie import with ~460 lines of new parsing and UI code. Unresolved High severity finding about cookie domain handling that could widen cookies to subdomains, plus the "Open System Settings" button appears non-functional due to URL scheme not being allowlisted.

You can customize Macroscope's approvability policy. Learn more.

@macroscopeapp macroscopeapp Bot left a comment

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.

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

Comment thread apps/desktop/src/preview/BrowserImport/SafariCookies.ts

@macroscopeapp macroscopeapp Bot left a comment

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.

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

Comment thread apps/desktop/src/preview/BrowserImport/SafariCookies.ts
Comment thread apps/desktop/src/preview/BrowserImport/Sources.ts
Comment on lines +146 to +147
url: `${secure ? "https" : "http"}://${host}${path || "/"}`,
name,
value,
domain,

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.

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

Suggested change
domain,
domain: domain.startsWith(".") ? domain : undefined,
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/desktop/src/preview/BrowserImport/SafariCookies.ts around line 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 `.`.

Comment thread apps/desktop/src/preview/BrowserImport/SafariCookies.ts
@juliusmarminge
juliusmarminge force-pushed the browser-import-safari branch 2 times, most recently from 51247f4 to 180da0b Compare August 17, 2026 08:35

@macroscopeapp macroscopeapp Bot left a comment

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.

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

Comment on lines +921 to +927
<AlertDialogClose
render={<Button />}
onClick={() => {
void readLocalApi()
?.shell.openExternal(FULL_DISK_ACCESS_SETTINGS_URL)
.catch(() => undefined);
}}

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.

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

@macroscopeapp macroscopeapp Bot left a comment

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.

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

Comment thread apps/web/src/components/settings/IntegrationsSettings.tsx Outdated
Comment thread apps/web/src/components/settings/IntegrationsSettings.tsx Outdated

@macroscopeapp macroscopeapp Bot left a comment

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.

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

Comment on lines +849 to +853
onOpenFullDiskAccessSettings={() =>
void readLocalApi()
?.shell.openExternal(FULL_DISK_ACCESS_SETTINGS_URL)
.catch(() => undefined)
}

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.

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

@cursor cursor Bot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

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

Comment thread apps/web/src/components/settings/IntegrationsSettings.tsx
* later.
*/
const FULL_DISK_ACCESS_SETTINGS_URL =
"x-apple.systempreferences:com.apple.preference.security?Privacy_AllFilesAccess";

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.

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.

Fix in Cursor Fix in Web

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

juliusmarminge and others added 6 commits August 17, 2026 12:03
Safari does not encrypt its cookies. It stores them in a proprietary
`Cookies.binarycookies` file whose protection is TCC rather than cryptography:
the file sits inside the app container, which only apps with Full Disk Access
may read. So the gate is a permission the user grants in System Settings, and
a denial is reported as exactly that rather than as a generic read failure.

Two details the format forces:

Timestamps count seconds from 2001-01-01, not the UNIX epoch, so every expiry
needs rebasing or cookies import as long expired.

The format predates SameSite and carries no equivalent field. Imported cookies
are marked Lax, the modern browser default — claiming "none" would widen the
scope of every cookie Safari ever set.

Safari keeps one jar for the whole app rather than per-profile, so it exposes
a single implicit profile, and it has no observable lock file since the jar is
written atomically.

The parser is covered by tests that build the binary format byte for byte,
including a multi-page file — Safari pages its jar, and a single-page reader
would silently return only the first slice. That coverage matters because the
real file cannot be read on this machine without the very permission the
feature asks for; the TCC path itself was verified against the live file,
which denies with EPERM and reports `needsFullDiskAccess`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every `nodeBuiltinImport:off` in the import module now says which builtin it
covers and why Effect has no equivalent, matching the neighbouring preload and
Playwright modules.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… of it

`Buffer.subarray` clamps rather than throwing, so every declared structure in
the binary format was taken on trust. An overlong page swallowed the following
page's bytes and pushed the cursor past the end, dropping every cookie after
the boundary from an import that still reported success. A record whose
declared size overran its page left its string offsets free to read the next
record's bytes as this cookie's value.

Pages, records, and string offsets are now bounds-checked against what the file
actually contains, and a mismatch fails the read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Matches the Chromium and Firefox readers: the failure carries which jar it was
for, so a Full Disk Access refusal is traceable rather than anonymous. Optional
because the parser raises before a path is in hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A Safari import with no Full Disk Access failed with the generic "cookie
database could not be read" instead of telling the user to grant access — and
no prompt appears, because macOS never prompts for Full Disk Access; the app is
added by hand.

The denial arrives as EPERM, which Effect tags `Unknown`, not
`PermissionDenied` (that is EACCES), so checking the tag alone never matched.
The underlying errno is checked too. Verified against the real jar: the reason
is now `needsFullDiskAccess`, which the renderer maps to the System Settings
instruction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Safari's cookies sit behind Full Disk Access, which no one has granted before
their first import — so it is a step in the flow, not a failure. When an import
comes back needing it, the wizard shows a screen that says what it's for, links
to the right System Settings pane, and — from an "I've turned it on" button —
runs the import itself, so the user never restarts from the menu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@macroscopeapp macroscopeapp Bot left a comment

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.

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

Comment on lines +197 to +206
return yield* Effect.try({
try: () => parseBinaryCookies(Buffer.from(contents)),
catch: (cause) =>
isSafariCookieReadError(cause)
? cause
: new SafariCookieReadError({
reason: "readFailed",
cookieDatabasePath: cookiePath,
cause,
}),

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.

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.

Posted via Macroscope — Effect Service Conventions

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

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant