feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox - #7260

Open
juliusmarminge wants to merge 22 commits into
browser-profile-importfrom
browser-import-more-sources
Open

feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox#7260
juliusmarminge wants to merge 22 commits into
browser-profile-importfrom
browser-import-more-sources

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Stacked on #7255 (browser-profile-import).

Expands cookie import to a source registry with Chromium and Firefox readers.

SourcemacOSWindowsLinux in this layer
Chrome, Edge, Brave, Vivaldi, OperaSupportedUnsupportedDetected; key support in #7261
Arc, HeliumSupportedUnsupportedUnsupported
FirefoxSupportedSupportedSupported

Firefox imports its default container; other containers are excluded so their sessions are not mixed. Discovery respects profiles.ini, validates relative paths, supports explicitly configured absolute profile directories, and ignores fallback entries without a cookie database. Counts use the same default-container filter as the reader.

Both engines share a consistent read-only SQLite snapshot path. Windows Chromium remains intentionally unsupported, preserving the Windows-tested contribution #7323.

Validation: focused source-discovery, path-validation, Firefox cookie/count, and SQLite snapshot fixtures. Cross-platform path tests run on macOS; they are not a substitute for another live Windows/Linux test.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents, coordinated through Codex.

Note

Add Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox import to desktop

  • Expands the BROWSER_IMPORT_SOURCES registry to include Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox, using a platform-aware BrowserImportPathContext to resolve user-data directories.
  • Adds a Firefox cookie reader that parses profiles.ini, handles schema-specific expiry and SameSite conversions, filters to default-container cookies, and uses Windows or POSIX lock probes to detect if the browser is running.
  • Extracts shared cookie contracts and SQLite snapshot logic into CookieDatabase.ts for use by both Chromium and Firefox engines.
  • Adds a profileLimitReached failure reason and ProfileLimitReachedError when profile creation or import persistence exceeds the configured maximum.
  • Risk: Chromium sources are now blocked on non-macOS platforms in BrowserImport.unavailableReason; out-of-tree consumers will receive unsupportedPlatform instead of attempting unsupported keychain reads.

Macroscope summarized ff29cce.


Note

Medium Risk
Touches cookie/session import, filesystem path validation, and keychain reads on macOS; Firefox lock probing spawns Python and may allow import while Firefox is running if the probe is unavailable.

Overview
Adds a multi-browser import registry (Chrome, Edge, Brave, Vivaldi, Opera, Arc, Helium, Firefox) with an engine split and BrowserImportPathContext (platform, home, Windows %APPDATA%) replacing the old sourcePaths helper.

Firefox gets a plaintext cookies.sqlite reader, profiles.ini discovery with path hardening, default-container-only imports, schema-aware expiry/SameSite handling, and per-profile running detection (Linux lock symlinks, macOS .parentlock fcntl via a short Python probe, Windows parent.lock). HostProcessAddresses resolves local IPs for lock ownership checks.

Chromium sources share a chromiumSource factory (macOS + Linux paths; non-macOS Chromium import is gated as unsupportedPlatform until other key stores exist). Shared CookieDatabase holds ImportedCookie, WAL-safe snapshotCookieDatabase, and cookieScope. BrowserImport.importCookies branches on engine, skips keychain audit logs for Firefox, and maps Chromium unspecified SameSite to Electron unspecified instead of forcing Lax.

Contracts expand BROWSER_IMPORT_SOURCE_IDS; tests cover Firefox/Chromium readers, snapshots, locks, and discovery edge cases.

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

@coderabbitai

coderabbitaiBot 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: f3c31f7b-c598-4238-adee-9242d001ce25

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-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 16, 2026
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@github-actions

github-actionsBot 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.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.4 KiB15.1 KiB
CodexThread snapshot wire6.9 KiB7.3 KiB
CodexLive turn WebSocket wire6.6 KiB7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB66.4 KiB
CodexLive turn messages1021
ClaudeTotal thread wire13.5 KiB15.1 KiB
ClaudeThread snapshot wire6.9 KiB7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB66.4 KiB
ClaudeLive turn messages1021

Baseline: unavailable · PR result: ff29cce · 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: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

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

@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from bc1215b to 973b17eCompareAugust 16, 2026 22:08
@github-actionsgithub-actionsBot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026

@macroscopeappmacroscopeappBot 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 convention finding: the new Firefox cookie read path models its failure as an anonymous object literal instead of a Schema.TaggedErrorClass. Details inline.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/desktop/src/preview/BrowserImport/BrowserImport.ts
Comment threadapps/desktop/src/preview/BrowserImport/CookieDatabase.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 22:18
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a multi-browser cookie-import capability with new cross-platform discovery, database extraction, lock handling, Keychain interaction, and session-cookie writes. It also introduces a static-analysis diagnostic suppression in a new test, so the scope and sensitive-data handling warrant human review.

No code changes detected at ff29cce. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 973b17e to 1b132e4CompareAugust 16, 2026 22:53
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 3343565 to 56f1705CompareAugust 16, 2026 23:10

@macroscopeappmacroscopeappBot 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: one finding on the new FirefoxCookieReadError. The tagged-error union fix from the previous round looks good; what remains is the structural context the error captures.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 56f1705 to 4d48bd9CompareAugust 16, 2026 23:14
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 4d48bd9 to 0d85caaCompareAugust 16, 2026 23:22
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 7a8cf25 to 27037c9CompareAugust 16, 2026 23:51
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 27037c9 to 9328514CompareAugust 16, 2026 23:59
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
Comment threadapps/desktop/src/preview/BrowserImport/BrowserImport.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated

@cursorcursorBot 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 8360b0f. Configure here.

Comment threadpackages/shared/src/hostProcess.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
juliusmarmingeand others added 22 commits September 2, 2026 16:00
…Firefox
Generalises the importer from one hardcoded browser to a source registry with
two engines.
Chromium forks are table entries: Chrome, Edge, Brave, Vivaldi, Opera, Arc and
Helium all share the existing extractor and differ only in their paths and
keychain coordinates. Those coordinates are pinned per fork rather than
derived, because the forks disagree — Helium uses "Helium Storage Key" /
"Helium" where the others use "<Name> Safe Storage" / "<Name>".
Firefox is a second engine. It stores cookies unencrypted in `cookies.sqlite`,
so there is no key to fetch and no consent prompt — that is Mozilla's design
choice, not a control being circumvented, and it is why Firefox works
identically on all three platforms while Chromium still needs a per-platform
credential store.
Paths resolve for macOS, Windows and Linux from an injected context rather
than from `process`, so a platform's layout can be checked without running on
it. Chromium off macOS still reports `unsupportedPlatform` until those key
stores land; Firefox does not.
The snapshot-before-read step moves to a shared module, since both engines
keep the database open with WAL and must never have the browser's own file
opened for writing.
Firefox has tests against a real `moz_cookies` fixture, including that the
source file is left untouched, and `profiles.ini` parsing covers the
`Install*` sections that name a default profile without describing one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox keeps its lock files inside each profile, not at the user-data root, so
the running check never found them and offered imports from a live, mid-write
database. It now walks the source's profiles and looks for all three names the
platforms use.
Firefox isolates cookies per container and per private window through
`originAttributes`. Electron has no equivalent, so importing them all collapsed
several identities onto one host/name/path and handed the profile whichever
container was written last. Only the default container is imported.
The sidecar copy no longer ignores every error alongside the missing-file case;
the new snapshot test caught that the earlier fix had landed on the
pre-extraction copy of this code rather than the shared module.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Firefox branch failed with an anonymous `{ reason, cause }` literal, and
typing the shared `read` value as that shape erased `ChromiumCookieReadError`'s
tag from the error channel — neither branch could then be handled with
`Effect.catchTags`.
`FirefoxCookieReadError` mirrors its Chromium counterpart, so the union stays
structurally identifiable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox keeps one cookie database per profile, so a failure carrying only a
reason cannot be traced back to the profile that produced it. The path is now a
structural attribute, matching `ChromiumCookieReadError`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox has one failure mode — its plaintext database would not open — so a
single-value `reason` literal encoded the same thing as the tag, and `cause`
was optional though every construction site wraps a real failure. The error now
carries the database path and a required cause, and `BrowserImport` supplies
the user-facing reason where it maps the union.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The union was discriminated with a `cause._tag` ternary inside `Effect.mapError`
even though both members are statically known tagged errors; `Effect.catchTags`
says the same thing without the manual check. The comment above `read` claiming
both carry a `reason` was stale — Firefox's no longer does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox schema 16 (Firefox 129) moved `moz_cookies.expiry` from seconds to
milliseconds — the migration is `UPDATE moz_cookies SET expiry = expiry * 1000`
— but the reader passed the value straight through as seconds, so persistent
cookies from a current Firefox were imported ~1000× too far in the future.
Older profiles still hold seconds, so the unit is decided by `PRAGMA
user_version` rather than assumed either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… browser
`isSourceRunning` treated `.parentlock` / `parent.lock` as proof Firefox held
the profile, but Firefox deliberately leaves those on disk after a clean exit
(as a last-used marker) and only releases the OS-level fcntl or handle lock —
so once Firefox had been used, the source reported `browserRunning` forever and
cookie import never proceeded.
Verified against Firefox's nsProfileLock.cpp: only the Linux `lock` symlink
(target `<ip>:[+]<pid>`) is unlinked on exit, so its presence plus a live pid
is the POSIX signal; a dead pid means a crash. Node has no fcntl probe, so
`.parentlock` carries no signal on POSIX. On Windows the held handle denies our
open as `Busy`, which is the signal there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s detected
Follow-ups to the leftover-lock fix. On macOS a running Firefox writes nothing
but an empty `.parentlock` held with an fcntl lock — no symlink, no pid — so
reading only the Linux `lock` symlink missed it entirely and offered imports
from a live, mid-write database. Node exposes no fcntl, so a throwaway
`python3` child tries a non-blocking F_SETLK and reports whether it blocks;
any failure to get a clean answer counts as held, so a probe problem can
never unlock a live profile. The Linux symlink still short-circuits first.
The symlink's owner half is honoured too: a non-loopback address is a shared
profile locked from another machine, whose pid cannot be probed here, so it
stays held. Also drops a Safari paragraph from `entryExists`' doc that
described `databaseFileExists`' concern, not this helper's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… Windows lock probe
A Firefox import logged "Reading browser cookie key from the keychain" with the executable path even though it never touches a keychain, putting a false security-sensitive event in the audit trail; the log now sits behind the Chromium engine check. Also narrows isLockHeld to the Windows probe its doc describes (its only caller already guards on win32) and deletes entryExists, which that dead branch was the last use of.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ock symlink
The owner check treated only 127.0.0.1 and ::1 as ours, but Firefox writes
whatever its resolver returns for the machine's hostname — 127.0.1.1 on
Debian-style hosts, a LAN address elsewhere, loopback only when the lookup
fails. A crashed Firefox on such a host left a symlink the check read as
foreign and held forever, blocking import. The owner is now matched against
every address the machine answers to, via a new `HostProcessAddresses`
reference that mirrors `HostProcessHostname` and is injectable in tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…name-resolved addresses as local
Two follow-ups on the Firefox lock detection.
A Dock-launched app has launchd's bare PATH, so `python3` could go unfound;
the probe now names `/usr/bin/python3` absolutely first. More importantly,
"held" is now only ever the script's own verdict: a probe that never ran —
no interpreter, or Apple's shim on a Mac without the developer tools, which
exits non-zero after prompting to install — is the probe being unavailable,
not evidence about the lock, and falls back to "not held" rather than
blocking Firefox import on that machine for good. The SQLite snapshot copes
with a live WAL as it does for every other engine.
And `HostProcessAddresses` collected only interface addresses, so a
hostname mapped in /etc/hosts to an address no interface carries — Debian's
127.0.1.1 — read as foreign, holding a crashed Firefox's lock forever. The
set now also includes what the resolver returns for the machine's hostname,
which is exactly what Firefox writes into the lock.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox stores nsICookie::SAMESITE_UNSET (256) for a cookie that carried no
SameSite attribute, distinct from SAMESITE_NONE (0), an explicit opt-in to
cross-site use. The reader collapsed the unknown value onto Lax, and the
reviewed suggestion would have made it None — both change the cookie's
meaning. `ImportedCookie` now carries Electron's own `unspecified`, which lets
the target browser apply its default exactly as the source did, and both
engines map their "no attribute" sentinel (Firefox 256, Chromium -1) to it.
Also drops a doubled `/**` opener left above `cookieDatabaseCandidatePaths`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rows from before schema 9 hold NULL in sameSite, which failed decoding
and aborted the whole import. Schemas 10-14 also keep the declared value
in rawSameSite beside a Lax default; apply Firefox's own schema-15
migration rule so an undeclared cookie imports as unspecified rather
than as an explicit Lax.
The hostname DNS lookup ran once per profile and even on Windows, where
the symlink lock that needs it never exists.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox - #7260

Open
juliusmarminge wants to merge 22 commits into
browser-profile-importfrom
browser-import-more-sources
Open

feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox#7260
juliusmarminge wants to merge 22 commits into
browser-profile-importfrom
browser-import-more-sources

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Stacked on #7255 (browser-profile-import).

Expands cookie import to a source registry with Chromium and Firefox readers.

SourcemacOSWindowsLinux in this layer
Chrome, Edge, Brave, Vivaldi, OperaSupportedUnsupportedDetected; key support in #7261
Arc, HeliumSupportedUnsupportedUnsupported
FirefoxSupportedSupportedSupported

Firefox imports its default container; other containers are excluded so their sessions are not mixed. Discovery respects profiles.ini, validates relative paths, supports explicitly configured absolute profile directories, and ignores fallback entries without a cookie database. Counts use the same default-container filter as the reader.

Both engines share a consistent read-only SQLite snapshot path. Windows Chromium remains intentionally unsupported, preserving the Windows-tested contribution #7323.

Validation: focused source-discovery, path-validation, Firefox cookie/count, and SQLite snapshot fixtures. Cross-platform path tests run on macOS; they are not a substitute for another live Windows/Linux test.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents, coordinated through Codex.

Note

Add Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox import to desktop

  • Expands the BROWSER_IMPORT_SOURCES registry to include Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox, using a platform-aware BrowserImportPathContext to resolve user-data directories.
  • Adds a Firefox cookie reader that parses profiles.ini, handles schema-specific expiry and SameSite conversions, filters to default-container cookies, and uses Windows or POSIX lock probes to detect if the browser is running.
  • Extracts shared cookie contracts and SQLite snapshot logic into CookieDatabase.ts for use by both Chromium and Firefox engines.
  • Adds a profileLimitReached failure reason and ProfileLimitReachedError when profile creation or import persistence exceeds the configured maximum.
  • Risk: Chromium sources are now blocked on non-macOS platforms in BrowserImport.unavailableReason; out-of-tree consumers will receive unsupportedPlatform instead of attempting unsupported keychain reads.

Macroscope summarized ff29cce.


Note

Medium Risk
Touches cookie/session import, filesystem path validation, and keychain reads on macOS; Firefox lock probing spawns Python and may allow import while Firefox is running if the probe is unavailable.

Overview
Adds a multi-browser import registry (Chrome, Edge, Brave, Vivaldi, Opera, Arc, Helium, Firefox) with an engine split and BrowserImportPathContext (platform, home, Windows %APPDATA%) replacing the old sourcePaths helper.

Firefox gets a plaintext cookies.sqlite reader, profiles.ini discovery with path hardening, default-container-only imports, schema-aware expiry/SameSite handling, and per-profile running detection (Linux lock symlinks, macOS .parentlock fcntl via a short Python probe, Windows parent.lock). HostProcessAddresses resolves local IPs for lock ownership checks.

Chromium sources share a chromiumSource factory (macOS + Linux paths; non-macOS Chromium import is gated as unsupportedPlatform until other key stores exist). Shared CookieDatabase holds ImportedCookie, WAL-safe snapshotCookieDatabase, and cookieScope. BrowserImport.importCookies branches on engine, skips keychain audit logs for Firefox, and maps Chromium unspecified SameSite to Electron unspecified instead of forcing Lax.

Contracts expand BROWSER_IMPORT_SOURCE_IDS; tests cover Firefox/Chromium readers, snapshots, locks, and discovery edge cases.

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

@coderabbitai

coderabbitaiBot 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: f3c31f7b-c598-4238-adee-9242d001ce25

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-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 16, 2026
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@github-actions

github-actionsBot 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.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.4 KiB15.1 KiB
CodexThread snapshot wire6.9 KiB7.3 KiB
CodexLive turn WebSocket wire6.6 KiB7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB66.4 KiB
CodexLive turn messages1021
ClaudeTotal thread wire13.5 KiB15.1 KiB
ClaudeThread snapshot wire6.9 KiB7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB66.4 KiB
ClaudeLive turn messages1021

Baseline: unavailable · PR result: ff29cce · 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: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

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

@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from bc1215b to 973b17eCompareAugust 16, 2026 22:08
@github-actionsgithub-actionsBot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026

@macroscopeappmacroscopeappBot 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 convention finding: the new Firefox cookie read path models its failure as an anonymous object literal instead of a Schema.TaggedErrorClass. Details inline.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/desktop/src/preview/BrowserImport/BrowserImport.ts
Comment threadapps/desktop/src/preview/BrowserImport/CookieDatabase.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 22:18
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a multi-browser cookie-import capability with new cross-platform discovery, database extraction, lock handling, Keychain interaction, and session-cookie writes. It also introduces a static-analysis diagnostic suppression in a new test, so the scope and sensitive-data handling warrant human review.

No code changes detected at ff29cce. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 973b17e to 1b132e4CompareAugust 16, 2026 22:53
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 3343565 to 56f1705CompareAugust 16, 2026 23:10

@macroscopeappmacroscopeappBot 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: one finding on the new FirefoxCookieReadError. The tagged-error union fix from the previous round looks good; what remains is the structural context the error captures.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 56f1705 to 4d48bd9CompareAugust 16, 2026 23:14
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 4d48bd9 to 0d85caaCompareAugust 16, 2026 23:22
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 7a8cf25 to 27037c9CompareAugust 16, 2026 23:51
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 27037c9 to 9328514CompareAugust 16, 2026 23:59
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
Comment threadapps/desktop/src/preview/BrowserImport/BrowserImport.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated

@cursorcursorBot 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 8360b0f. Configure here.

Comment threadpackages/shared/src/hostProcess.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
juliusmarmingeand others added 22 commits September 2, 2026 16:00
…Firefox
Generalises the importer from one hardcoded browser to a source registry with
two engines.
Chromium forks are table entries: Chrome, Edge, Brave, Vivaldi, Opera, Arc and
Helium all share the existing extractor and differ only in their paths and
keychain coordinates. Those coordinates are pinned per fork rather than
derived, because the forks disagree — Helium uses "Helium Storage Key" /
"Helium" where the others use "<Name> Safe Storage" / "<Name>".
Firefox is a second engine. It stores cookies unencrypted in `cookies.sqlite`,
so there is no key to fetch and no consent prompt — that is Mozilla's design
choice, not a control being circumvented, and it is why Firefox works
identically on all three platforms while Chromium still needs a per-platform
credential store.
Paths resolve for macOS, Windows and Linux from an injected context rather
than from `process`, so a platform's layout can be checked without running on
it. Chromium off macOS still reports `unsupportedPlatform` until those key
stores land; Firefox does not.
The snapshot-before-read step moves to a shared module, since both engines
keep the database open with WAL and must never have the browser's own file
opened for writing.
Firefox has tests against a real `moz_cookies` fixture, including that the
source file is left untouched, and `profiles.ini` parsing covers the
`Install*` sections that name a default profile without describing one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox keeps its lock files inside each profile, not at the user-data root, so
the running check never found them and offered imports from a live, mid-write
database. It now walks the source's profiles and looks for all three names the
platforms use.
Firefox isolates cookies per container and per private window through
`originAttributes`. Electron has no equivalent, so importing them all collapsed
several identities onto one host/name/path and handed the profile whichever
container was written last. Only the default container is imported.
The sidecar copy no longer ignores every error alongside the missing-file case;
the new snapshot test caught that the earlier fix had landed on the
pre-extraction copy of this code rather than the shared module.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Firefox branch failed with an anonymous `{ reason, cause }` literal, and
typing the shared `read` value as that shape erased `ChromiumCookieReadError`'s
tag from the error channel — neither branch could then be handled with
`Effect.catchTags`.
`FirefoxCookieReadError` mirrors its Chromium counterpart, so the union stays
structurally identifiable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox keeps one cookie database per profile, so a failure carrying only a
reason cannot be traced back to the profile that produced it. The path is now a
structural attribute, matching `ChromiumCookieReadError`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox has one failure mode — its plaintext database would not open — so a
single-value `reason` literal encoded the same thing as the tag, and `cause`
was optional though every construction site wraps a real failure. The error now
carries the database path and a required cause, and `BrowserImport` supplies
the user-facing reason where it maps the union.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The union was discriminated with a `cause._tag` ternary inside `Effect.mapError`
even though both members are statically known tagged errors; `Effect.catchTags`
says the same thing without the manual check. The comment above `read` claiming
both carry a `reason` was stale — Firefox's no longer does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox schema 16 (Firefox 129) moved `moz_cookies.expiry` from seconds to
milliseconds — the migration is `UPDATE moz_cookies SET expiry = expiry * 1000`
— but the reader passed the value straight through as seconds, so persistent
cookies from a current Firefox were imported ~1000× too far in the future.
Older profiles still hold seconds, so the unit is decided by `PRAGMA
user_version` rather than assumed either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… browser
`isSourceRunning` treated `.parentlock` / `parent.lock` as proof Firefox held
the profile, but Firefox deliberately leaves those on disk after a clean exit
(as a last-used marker) and only releases the OS-level fcntl or handle lock —
so once Firefox had been used, the source reported `browserRunning` forever and
cookie import never proceeded.
Verified against Firefox's nsProfileLock.cpp: only the Linux `lock` symlink
(target `<ip>:[+]<pid>`) is unlinked on exit, so its presence plus a live pid
is the POSIX signal; a dead pid means a crash. Node has no fcntl probe, so
`.parentlock` carries no signal on POSIX. On Windows the held handle denies our
open as `Busy`, which is the signal there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s detected
Follow-ups to the leftover-lock fix. On macOS a running Firefox writes nothing
but an empty `.parentlock` held with an fcntl lock — no symlink, no pid — so
reading only the Linux `lock` symlink missed it entirely and offered imports
from a live, mid-write database. Node exposes no fcntl, so a throwaway
`python3` child tries a non-blocking F_SETLK and reports whether it blocks;
any failure to get a clean answer counts as held, so a probe problem can
never unlock a live profile. The Linux symlink still short-circuits first.
The symlink's owner half is honoured too: a non-loopback address is a shared
profile locked from another machine, whose pid cannot be probed here, so it
stays held. Also drops a Safari paragraph from `entryExists`' doc that
described `databaseFileExists`' concern, not this helper's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… Windows lock probe
A Firefox import logged "Reading browser cookie key from the keychain" with the executable path even though it never touches a keychain, putting a false security-sensitive event in the audit trail; the log now sits behind the Chromium engine check. Also narrows isLockHeld to the Windows probe its doc describes (its only caller already guards on win32) and deletes entryExists, which that dead branch was the last use of.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ock symlink
The owner check treated only 127.0.0.1 and ::1 as ours, but Firefox writes
whatever its resolver returns for the machine's hostname — 127.0.1.1 on
Debian-style hosts, a LAN address elsewhere, loopback only when the lookup
fails. A crashed Firefox on such a host left a symlink the check read as
foreign and held forever, blocking import. The owner is now matched against
every address the machine answers to, via a new `HostProcessAddresses`
reference that mirrors `HostProcessHostname` and is injectable in tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…name-resolved addresses as local
Two follow-ups on the Firefox lock detection.
A Dock-launched app has launchd's bare PATH, so `python3` could go unfound;
the probe now names `/usr/bin/python3` absolutely first. More importantly,
"held" is now only ever the script's own verdict: a probe that never ran —
no interpreter, or Apple's shim on a Mac without the developer tools, which
exits non-zero after prompting to install — is the probe being unavailable,
not evidence about the lock, and falls back to "not held" rather than
blocking Firefox import on that machine for good. The SQLite snapshot copes
with a live WAL as it does for every other engine.
And `HostProcessAddresses` collected only interface addresses, so a
hostname mapped in /etc/hosts to an address no interface carries — Debian's
127.0.1.1 — read as foreign, holding a crashed Firefox's lock forever. The
set now also includes what the resolver returns for the machine's hostname,
which is exactly what Firefox writes into the lock.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox stores nsICookie::SAMESITE_UNSET (256) for a cookie that carried no
SameSite attribute, distinct from SAMESITE_NONE (0), an explicit opt-in to
cross-site use. The reader collapsed the unknown value onto Lax, and the
reviewed suggestion would have made it None — both change the cookie's
meaning. `ImportedCookie` now carries Electron's own `unspecified`, which lets
the target browser apply its default exactly as the source did, and both
engines map their "no attribute" sentinel (Firefox 256, Chromium -1) to it.
Also drops a doubled `/**` opener left above `cookieDatabaseCandidatePaths`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rows from before schema 9 hold NULL in sameSite, which failed decoding
and aborted the whole import. Schemas 10-14 also keep the declared value
in rawSameSite beside a Lax default; apply Firefox's own schema-15
migration rule so an undeclared cookie imports as unspecified rather
than as an explicit Lax.
The hostname DNS lookup ran once per profile and even on Windows, where
the symlink lock that needs it never exists.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox - #7260

Open
juliusmarminge wants to merge 22 commits into
browser-profile-importfrom
browser-import-more-sources
Open

feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox#7260
juliusmarminge wants to merge 22 commits into
browser-profile-importfrom
browser-import-more-sources

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Stacked on #7255 (browser-profile-import).

Expands cookie import to a source registry with Chromium and Firefox readers.

SourcemacOSWindowsLinux in this layer
Chrome, Edge, Brave, Vivaldi, OperaSupportedUnsupportedDetected; key support in #7261
Arc, HeliumSupportedUnsupportedUnsupported
FirefoxSupportedSupportedSupported

Firefox imports its default container; other containers are excluded so their sessions are not mixed. Discovery respects profiles.ini, validates relative paths, supports explicitly configured absolute profile directories, and ignores fallback entries without a cookie database. Counts use the same default-container filter as the reader.

Both engines share a consistent read-only SQLite snapshot path. Windows Chromium remains intentionally unsupported, preserving the Windows-tested contribution #7323.

Validation: focused source-discovery, path-validation, Firefox cookie/count, and SQLite snapshot fixtures. Cross-platform path tests run on macOS; they are not a substitute for another live Windows/Linux test.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents, coordinated through Codex.

Note

Add Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox import to desktop

  • Expands the BROWSER_IMPORT_SOURCES registry to include Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox, using a platform-aware BrowserImportPathContext to resolve user-data directories.
  • Adds a Firefox cookie reader that parses profiles.ini, handles schema-specific expiry and SameSite conversions, filters to default-container cookies, and uses Windows or POSIX lock probes to detect if the browser is running.
  • Extracts shared cookie contracts and SQLite snapshot logic into CookieDatabase.ts for use by both Chromium and Firefox engines.
  • Adds a profileLimitReached failure reason and ProfileLimitReachedError when profile creation or import persistence exceeds the configured maximum.
  • Risk: Chromium sources are now blocked on non-macOS platforms in BrowserImport.unavailableReason; out-of-tree consumers will receive unsupportedPlatform instead of attempting unsupported keychain reads.

Macroscope summarized ff29cce.


Note

Medium Risk
Touches cookie/session import, filesystem path validation, and keychain reads on macOS; Firefox lock probing spawns Python and may allow import while Firefox is running if the probe is unavailable.

Overview
Adds a multi-browser import registry (Chrome, Edge, Brave, Vivaldi, Opera, Arc, Helium, Firefox) with an engine split and BrowserImportPathContext (platform, home, Windows %APPDATA%) replacing the old sourcePaths helper.

Firefox gets a plaintext cookies.sqlite reader, profiles.ini discovery with path hardening, default-container-only imports, schema-aware expiry/SameSite handling, and per-profile running detection (Linux lock symlinks, macOS .parentlock fcntl via a short Python probe, Windows parent.lock). HostProcessAddresses resolves local IPs for lock ownership checks.

Chromium sources share a chromiumSource factory (macOS + Linux paths; non-macOS Chromium import is gated as unsupportedPlatform until other key stores exist). Shared CookieDatabase holds ImportedCookie, WAL-safe snapshotCookieDatabase, and cookieScope. BrowserImport.importCookies branches on engine, skips keychain audit logs for Firefox, and maps Chromium unspecified SameSite to Electron unspecified instead of forcing Lax.

Contracts expand BROWSER_IMPORT_SOURCE_IDS; tests cover Firefox/Chromium readers, snapshots, locks, and discovery edge cases.

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

@coderabbitai

coderabbitaiBot 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: f3c31f7b-c598-4238-adee-9242d001ce25

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-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 16, 2026
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@github-actions

github-actionsBot 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.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.4 KiB15.1 KiB
CodexThread snapshot wire6.9 KiB7.3 KiB
CodexLive turn WebSocket wire6.6 KiB7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB66.4 KiB
CodexLive turn messages1021
ClaudeTotal thread wire13.5 KiB15.1 KiB
ClaudeThread snapshot wire6.9 KiB7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB66.4 KiB
ClaudeLive turn messages1021

Baseline: unavailable · PR result: ff29cce · 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: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

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

@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from bc1215b to 973b17eCompareAugust 16, 2026 22:08
@github-actionsgithub-actionsBot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026

@macroscopeappmacroscopeappBot 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 convention finding: the new Firefox cookie read path models its failure as an anonymous object literal instead of a Schema.TaggedErrorClass. Details inline.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/desktop/src/preview/BrowserImport/BrowserImport.ts
Comment threadapps/desktop/src/preview/BrowserImport/CookieDatabase.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 22:18
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a multi-browser cookie-import capability with new cross-platform discovery, database extraction, lock handling, Keychain interaction, and session-cookie writes. It also introduces a static-analysis diagnostic suppression in a new test, so the scope and sensitive-data handling warrant human review.

No code changes detected at ff29cce. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 973b17e to 1b132e4CompareAugust 16, 2026 22:53
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 3343565 to 56f1705CompareAugust 16, 2026 23:10

@macroscopeappmacroscopeappBot 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: one finding on the new FirefoxCookieReadError. The tagged-error union fix from the previous round looks good; what remains is the structural context the error captures.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 56f1705 to 4d48bd9CompareAugust 16, 2026 23:14
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 4d48bd9 to 0d85caaCompareAugust 16, 2026 23:22
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 7a8cf25 to 27037c9CompareAugust 16, 2026 23:51
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 27037c9 to 9328514CompareAugust 16, 2026 23:59
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
Comment threadapps/desktop/src/preview/BrowserImport/BrowserImport.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated

@cursorcursorBot 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 8360b0f. Configure here.

Comment threadpackages/shared/src/hostProcess.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
juliusmarmingeand others added 22 commits September 2, 2026 16:00
…Firefox
Generalises the importer from one hardcoded browser to a source registry with
two engines.
Chromium forks are table entries: Chrome, Edge, Brave, Vivaldi, Opera, Arc and
Helium all share the existing extractor and differ only in their paths and
keychain coordinates. Those coordinates are pinned per fork rather than
derived, because the forks disagree — Helium uses "Helium Storage Key" /
"Helium" where the others use "<Name> Safe Storage" / "<Name>".
Firefox is a second engine. It stores cookies unencrypted in `cookies.sqlite`,
so there is no key to fetch and no consent prompt — that is Mozilla's design
choice, not a control being circumvented, and it is why Firefox works
identically on all three platforms while Chromium still needs a per-platform
credential store.
Paths resolve for macOS, Windows and Linux from an injected context rather
than from `process`, so a platform's layout can be checked without running on
it. Chromium off macOS still reports `unsupportedPlatform` until those key
stores land; Firefox does not.
The snapshot-before-read step moves to a shared module, since both engines
keep the database open with WAL and must never have the browser's own file
opened for writing.
Firefox has tests against a real `moz_cookies` fixture, including that the
source file is left untouched, and `profiles.ini` parsing covers the
`Install*` sections that name a default profile without describing one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox keeps its lock files inside each profile, not at the user-data root, so
the running check never found them and offered imports from a live, mid-write
database. It now walks the source's profiles and looks for all three names the
platforms use.
Firefox isolates cookies per container and per private window through
`originAttributes`. Electron has no equivalent, so importing them all collapsed
several identities onto one host/name/path and handed the profile whichever
container was written last. Only the default container is imported.
The sidecar copy no longer ignores every error alongside the missing-file case;
the new snapshot test caught that the earlier fix had landed on the
pre-extraction copy of this code rather than the shared module.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Firefox branch failed with an anonymous `{ reason, cause }` literal, and
typing the shared `read` value as that shape erased `ChromiumCookieReadError`'s
tag from the error channel — neither branch could then be handled with
`Effect.catchTags`.
`FirefoxCookieReadError` mirrors its Chromium counterpart, so the union stays
structurally identifiable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox keeps one cookie database per profile, so a failure carrying only a
reason cannot be traced back to the profile that produced it. The path is now a
structural attribute, matching `ChromiumCookieReadError`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox has one failure mode — its plaintext database would not open — so a
single-value `reason` literal encoded the same thing as the tag, and `cause`
was optional though every construction site wraps a real failure. The error now
carries the database path and a required cause, and `BrowserImport` supplies
the user-facing reason where it maps the union.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The union was discriminated with a `cause._tag` ternary inside `Effect.mapError`
even though both members are statically known tagged errors; `Effect.catchTags`
says the same thing without the manual check. The comment above `read` claiming
both carry a `reason` was stale — Firefox's no longer does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox schema 16 (Firefox 129) moved `moz_cookies.expiry` from seconds to
milliseconds — the migration is `UPDATE moz_cookies SET expiry = expiry * 1000`
— but the reader passed the value straight through as seconds, so persistent
cookies from a current Firefox were imported ~1000× too far in the future.
Older profiles still hold seconds, so the unit is decided by `PRAGMA
user_version` rather than assumed either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… browser
`isSourceRunning` treated `.parentlock` / `parent.lock` as proof Firefox held
the profile, but Firefox deliberately leaves those on disk after a clean exit
(as a last-used marker) and only releases the OS-level fcntl or handle lock —
so once Firefox had been used, the source reported `browserRunning` forever and
cookie import never proceeded.
Verified against Firefox's nsProfileLock.cpp: only the Linux `lock` symlink
(target `<ip>:[+]<pid>`) is unlinked on exit, so its presence plus a live pid
is the POSIX signal; a dead pid means a crash. Node has no fcntl probe, so
`.parentlock` carries no signal on POSIX. On Windows the held handle denies our
open as `Busy`, which is the signal there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s detected
Follow-ups to the leftover-lock fix. On macOS a running Firefox writes nothing
but an empty `.parentlock` held with an fcntl lock — no symlink, no pid — so
reading only the Linux `lock` symlink missed it entirely and offered imports
from a live, mid-write database. Node exposes no fcntl, so a throwaway
`python3` child tries a non-blocking F_SETLK and reports whether it blocks;
any failure to get a clean answer counts as held, so a probe problem can
never unlock a live profile. The Linux symlink still short-circuits first.
The symlink's owner half is honoured too: a non-loopback address is a shared
profile locked from another machine, whose pid cannot be probed here, so it
stays held. Also drops a Safari paragraph from `entryExists`' doc that
described `databaseFileExists`' concern, not this helper's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… Windows lock probe
A Firefox import logged "Reading browser cookie key from the keychain" with the executable path even though it never touches a keychain, putting a false security-sensitive event in the audit trail; the log now sits behind the Chromium engine check. Also narrows isLockHeld to the Windows probe its doc describes (its only caller already guards on win32) and deletes entryExists, which that dead branch was the last use of.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ock symlink
The owner check treated only 127.0.0.1 and ::1 as ours, but Firefox writes
whatever its resolver returns for the machine's hostname — 127.0.1.1 on
Debian-style hosts, a LAN address elsewhere, loopback only when the lookup
fails. A crashed Firefox on such a host left a symlink the check read as
foreign and held forever, blocking import. The owner is now matched against
every address the machine answers to, via a new `HostProcessAddresses`
reference that mirrors `HostProcessHostname` and is injectable in tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…name-resolved addresses as local
Two follow-ups on the Firefox lock detection.
A Dock-launched app has launchd's bare PATH, so `python3` could go unfound;
the probe now names `/usr/bin/python3` absolutely first. More importantly,
"held" is now only ever the script's own verdict: a probe that never ran —
no interpreter, or Apple's shim on a Mac without the developer tools, which
exits non-zero after prompting to install — is the probe being unavailable,
not evidence about the lock, and falls back to "not held" rather than
blocking Firefox import on that machine for good. The SQLite snapshot copes
with a live WAL as it does for every other engine.
And `HostProcessAddresses` collected only interface addresses, so a
hostname mapped in /etc/hosts to an address no interface carries — Debian's
127.0.1.1 — read as foreign, holding a crashed Firefox's lock forever. The
set now also includes what the resolver returns for the machine's hostname,
which is exactly what Firefox writes into the lock.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox stores nsICookie::SAMESITE_UNSET (256) for a cookie that carried no
SameSite attribute, distinct from SAMESITE_NONE (0), an explicit opt-in to
cross-site use. The reader collapsed the unknown value onto Lax, and the
reviewed suggestion would have made it None — both change the cookie's
meaning. `ImportedCookie` now carries Electron's own `unspecified`, which lets
the target browser apply its default exactly as the source did, and both
engines map their "no attribute" sentinel (Firefox 256, Chromium -1) to it.
Also drops a doubled `/**` opener left above `cookieDatabaseCandidatePaths`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rows from before schema 9 hold NULL in sameSite, which failed decoding
and aborted the whole import. Schemas 10-14 also keep the declared value
in rawSameSite beside a Lax default; apply Firefox's own schema-15
migration rule so an undeclared cookie imports as unspecified rather
than as an explicit Lax.
The hostname DNS lookup ran once per profile and even on Windows, where
the symlink lock that needs it never exists.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox - #7260

Open
juliusmarminge wants to merge 22 commits into
browser-profile-importfrom
browser-import-more-sources
Open

feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox#7260
juliusmarminge wants to merge 22 commits into
browser-profile-importfrom
browser-import-more-sources

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Stacked on #7255 (browser-profile-import).

Expands cookie import to a source registry with Chromium and Firefox readers.

SourcemacOSWindowsLinux in this layer
Chrome, Edge, Brave, Vivaldi, OperaSupportedUnsupportedDetected; key support in #7261
Arc, HeliumSupportedUnsupportedUnsupported
FirefoxSupportedSupportedSupported

Firefox imports its default container; other containers are excluded so their sessions are not mixed. Discovery respects profiles.ini, validates relative paths, supports explicitly configured absolute profile directories, and ignores fallback entries without a cookie database. Counts use the same default-container filter as the reader.

Both engines share a consistent read-only SQLite snapshot path. Windows Chromium remains intentionally unsupported, preserving the Windows-tested contribution #7323.

Validation: focused source-discovery, path-validation, Firefox cookie/count, and SQLite snapshot fixtures. Cross-platform path tests run on macOS; they are not a substitute for another live Windows/Linux test.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents, coordinated through Codex.

Note

Add Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox import to desktop

  • Expands the BROWSER_IMPORT_SOURCES registry to include Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox, using a platform-aware BrowserImportPathContext to resolve user-data directories.
  • Adds a Firefox cookie reader that parses profiles.ini, handles schema-specific expiry and SameSite conversions, filters to default-container cookies, and uses Windows or POSIX lock probes to detect if the browser is running.
  • Extracts shared cookie contracts and SQLite snapshot logic into CookieDatabase.ts for use by both Chromium and Firefox engines.
  • Adds a profileLimitReached failure reason and ProfileLimitReachedError when profile creation or import persistence exceeds the configured maximum.
  • Risk: Chromium sources are now blocked on non-macOS platforms in BrowserImport.unavailableReason; out-of-tree consumers will receive unsupportedPlatform instead of attempting unsupported keychain reads.

Macroscope summarized ff29cce.


Note

Medium Risk
Touches cookie/session import, filesystem path validation, and keychain reads on macOS; Firefox lock probing spawns Python and may allow import while Firefox is running if the probe is unavailable.

Overview
Adds a multi-browser import registry (Chrome, Edge, Brave, Vivaldi, Opera, Arc, Helium, Firefox) with an engine split and BrowserImportPathContext (platform, home, Windows %APPDATA%) replacing the old sourcePaths helper.

Firefox gets a plaintext cookies.sqlite reader, profiles.ini discovery with path hardening, default-container-only imports, schema-aware expiry/SameSite handling, and per-profile running detection (Linux lock symlinks, macOS .parentlock fcntl via a short Python probe, Windows parent.lock). HostProcessAddresses resolves local IPs for lock ownership checks.

Chromium sources share a chromiumSource factory (macOS + Linux paths; non-macOS Chromium import is gated as unsupportedPlatform until other key stores exist). Shared CookieDatabase holds ImportedCookie, WAL-safe snapshotCookieDatabase, and cookieScope. BrowserImport.importCookies branches on engine, skips keychain audit logs for Firefox, and maps Chromium unspecified SameSite to Electron unspecified instead of forcing Lax.

Contracts expand BROWSER_IMPORT_SOURCE_IDS; tests cover Firefox/Chromium readers, snapshots, locks, and discovery edge cases.

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

@coderabbitai

coderabbitaiBot 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: f3c31f7b-c598-4238-adee-9242d001ce25

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-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 16, 2026
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@github-actions

github-actionsBot 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.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.4 KiB15.1 KiB
CodexThread snapshot wire6.9 KiB7.3 KiB
CodexLive turn WebSocket wire6.6 KiB7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB66.4 KiB
CodexLive turn messages1021
ClaudeTotal thread wire13.5 KiB15.1 KiB
ClaudeThread snapshot wire6.9 KiB7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB66.4 KiB
ClaudeLive turn messages1021

Baseline: unavailable · PR result: ff29cce · 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: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

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

@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from bc1215b to 973b17eCompareAugust 16, 2026 22:08
@github-actionsgithub-actionsBot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026

@macroscopeappmacroscopeappBot 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 convention finding: the new Firefox cookie read path models its failure as an anonymous object literal instead of a Schema.TaggedErrorClass. Details inline.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/desktop/src/preview/BrowserImport/BrowserImport.ts
Comment threadapps/desktop/src/preview/BrowserImport/CookieDatabase.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 22:18
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a multi-browser cookie-import capability with new cross-platform discovery, database extraction, lock handling, Keychain interaction, and session-cookie writes. It also introduces a static-analysis diagnostic suppression in a new test, so the scope and sensitive-data handling warrant human review.

No code changes detected at ff29cce. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 973b17e to 1b132e4CompareAugust 16, 2026 22:53
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 3343565 to 56f1705CompareAugust 16, 2026 23:10

@macroscopeappmacroscopeappBot 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: one finding on the new FirefoxCookieReadError. The tagged-error union fix from the previous round looks good; what remains is the structural context the error captures.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 56f1705 to 4d48bd9CompareAugust 16, 2026 23:14
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 4d48bd9 to 0d85caaCompareAugust 16, 2026 23:22
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 7a8cf25 to 27037c9CompareAugust 16, 2026 23:51
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 27037c9 to 9328514CompareAugust 16, 2026 23:59
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
Comment threadapps/desktop/src/preview/BrowserImport/BrowserImport.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated

@cursorcursorBot 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 8360b0f. Configure here.

Comment threadpackages/shared/src/hostProcess.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
juliusmarmingeand others added 22 commits September 2, 2026 16:00
…Firefox
Generalises the importer from one hardcoded browser to a source registry with
two engines.
Chromium forks are table entries: Chrome, Edge, Brave, Vivaldi, Opera, Arc and
Helium all share the existing extractor and differ only in their paths and
keychain coordinates. Those coordinates are pinned per fork rather than
derived, because the forks disagree — Helium uses "Helium Storage Key" /
"Helium" where the others use "<Name> Safe Storage" / "<Name>".
Firefox is a second engine. It stores cookies unencrypted in `cookies.sqlite`,
so there is no key to fetch and no consent prompt — that is Mozilla's design
choice, not a control being circumvented, and it is why Firefox works
identically on all three platforms while Chromium still needs a per-platform
credential store.
Paths resolve for macOS, Windows and Linux from an injected context rather
than from `process`, so a platform's layout can be checked without running on
it. Chromium off macOS still reports `unsupportedPlatform` until those key
stores land; Firefox does not.
The snapshot-before-read step moves to a shared module, since both engines
keep the database open with WAL and must never have the browser's own file
opened for writing.
Firefox has tests against a real `moz_cookies` fixture, including that the
source file is left untouched, and `profiles.ini` parsing covers the
`Install*` sections that name a default profile without describing one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox keeps its lock files inside each profile, not at the user-data root, so
the running check never found them and offered imports from a live, mid-write
database. It now walks the source's profiles and looks for all three names the
platforms use.
Firefox isolates cookies per container and per private window through
`originAttributes`. Electron has no equivalent, so importing them all collapsed
several identities onto one host/name/path and handed the profile whichever
container was written last. Only the default container is imported.
The sidecar copy no longer ignores every error alongside the missing-file case;
the new snapshot test caught that the earlier fix had landed on the
pre-extraction copy of this code rather than the shared module.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Firefox branch failed with an anonymous `{ reason, cause }` literal, and
typing the shared `read` value as that shape erased `ChromiumCookieReadError`'s
tag from the error channel — neither branch could then be handled with
`Effect.catchTags`.
`FirefoxCookieReadError` mirrors its Chromium counterpart, so the union stays
structurally identifiable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox keeps one cookie database per profile, so a failure carrying only a
reason cannot be traced back to the profile that produced it. The path is now a
structural attribute, matching `ChromiumCookieReadError`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox has one failure mode — its plaintext database would not open — so a
single-value `reason` literal encoded the same thing as the tag, and `cause`
was optional though every construction site wraps a real failure. The error now
carries the database path and a required cause, and `BrowserImport` supplies
the user-facing reason where it maps the union.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The union was discriminated with a `cause._tag` ternary inside `Effect.mapError`
even though both members are statically known tagged errors; `Effect.catchTags`
says the same thing without the manual check. The comment above `read` claiming
both carry a `reason` was stale — Firefox's no longer does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox schema 16 (Firefox 129) moved `moz_cookies.expiry` from seconds to
milliseconds — the migration is `UPDATE moz_cookies SET expiry = expiry * 1000`
— but the reader passed the value straight through as seconds, so persistent
cookies from a current Firefox were imported ~1000× too far in the future.
Older profiles still hold seconds, so the unit is decided by `PRAGMA
user_version` rather than assumed either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… browser
`isSourceRunning` treated `.parentlock` / `parent.lock` as proof Firefox held
the profile, but Firefox deliberately leaves those on disk after a clean exit
(as a last-used marker) and only releases the OS-level fcntl or handle lock —
so once Firefox had been used, the source reported `browserRunning` forever and
cookie import never proceeded.
Verified against Firefox's nsProfileLock.cpp: only the Linux `lock` symlink
(target `<ip>:[+]<pid>`) is unlinked on exit, so its presence plus a live pid
is the POSIX signal; a dead pid means a crash. Node has no fcntl probe, so
`.parentlock` carries no signal on POSIX. On Windows the held handle denies our
open as `Busy`, which is the signal there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s detected
Follow-ups to the leftover-lock fix. On macOS a running Firefox writes nothing
but an empty `.parentlock` held with an fcntl lock — no symlink, no pid — so
reading only the Linux `lock` symlink missed it entirely and offered imports
from a live, mid-write database. Node exposes no fcntl, so a throwaway
`python3` child tries a non-blocking F_SETLK and reports whether it blocks;
any failure to get a clean answer counts as held, so a probe problem can
never unlock a live profile. The Linux symlink still short-circuits first.
The symlink's owner half is honoured too: a non-loopback address is a shared
profile locked from another machine, whose pid cannot be probed here, so it
stays held. Also drops a Safari paragraph from `entryExists`' doc that
described `databaseFileExists`' concern, not this helper's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… Windows lock probe
A Firefox import logged "Reading browser cookie key from the keychain" with the executable path even though it never touches a keychain, putting a false security-sensitive event in the audit trail; the log now sits behind the Chromium engine check. Also narrows isLockHeld to the Windows probe its doc describes (its only caller already guards on win32) and deletes entryExists, which that dead branch was the last use of.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ock symlink
The owner check treated only 127.0.0.1 and ::1 as ours, but Firefox writes
whatever its resolver returns for the machine's hostname — 127.0.1.1 on
Debian-style hosts, a LAN address elsewhere, loopback only when the lookup
fails. A crashed Firefox on such a host left a symlink the check read as
foreign and held forever, blocking import. The owner is now matched against
every address the machine answers to, via a new `HostProcessAddresses`
reference that mirrors `HostProcessHostname` and is injectable in tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…name-resolved addresses as local
Two follow-ups on the Firefox lock detection.
A Dock-launched app has launchd's bare PATH, so `python3` could go unfound;
the probe now names `/usr/bin/python3` absolutely first. More importantly,
"held" is now only ever the script's own verdict: a probe that never ran —
no interpreter, or Apple's shim on a Mac without the developer tools, which
exits non-zero after prompting to install — is the probe being unavailable,
not evidence about the lock, and falls back to "not held" rather than
blocking Firefox import on that machine for good. The SQLite snapshot copes
with a live WAL as it does for every other engine.
And `HostProcessAddresses` collected only interface addresses, so a
hostname mapped in /etc/hosts to an address no interface carries — Debian's
127.0.1.1 — read as foreign, holding a crashed Firefox's lock forever. The
set now also includes what the resolver returns for the machine's hostname,
which is exactly what Firefox writes into the lock.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox stores nsICookie::SAMESITE_UNSET (256) for a cookie that carried no
SameSite attribute, distinct from SAMESITE_NONE (0), an explicit opt-in to
cross-site use. The reader collapsed the unknown value onto Lax, and the
reviewed suggestion would have made it None — both change the cookie's
meaning. `ImportedCookie` now carries Electron's own `unspecified`, which lets
the target browser apply its default exactly as the source did, and both
engines map their "no attribute" sentinel (Firefox 256, Chromium -1) to it.
Also drops a doubled `/**` opener left above `cookieDatabaseCandidatePaths`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rows from before schema 9 hold NULL in sameSite, which failed decoding
and aborted the whole import. Schemas 10-14 also keep the declared value
in rawSameSite beside a Lax default; apply Firefox's own schema-15
migration rule so an undeclared cookie imports as unspecified rather
than as an explicit Lax.
The hostname DNS lookup ran once per profile and even on Windows, where
the symlink lock that needs it never exists.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox - #7260

Open
juliusmarminge wants to merge 22 commits into
browser-profile-importfrom
browser-import-more-sources
Open

feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox#7260
juliusmarminge wants to merge 22 commits into
browser-profile-importfrom
browser-import-more-sources

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Stacked on #7255 (browser-profile-import).

Expands cookie import to a source registry with Chromium and Firefox readers.

SourcemacOSWindowsLinux in this layer
Chrome, Edge, Brave, Vivaldi, OperaSupportedUnsupportedDetected; key support in #7261
Arc, HeliumSupportedUnsupportedUnsupported
FirefoxSupportedSupportedSupported

Firefox imports its default container; other containers are excluded so their sessions are not mixed. Discovery respects profiles.ini, validates relative paths, supports explicitly configured absolute profile directories, and ignores fallback entries without a cookie database. Counts use the same default-container filter as the reader.

Both engines share a consistent read-only SQLite snapshot path. Windows Chromium remains intentionally unsupported, preserving the Windows-tested contribution #7323.

Validation: focused source-discovery, path-validation, Firefox cookie/count, and SQLite snapshot fixtures. Cross-platform path tests run on macOS; they are not a substitute for another live Windows/Linux test.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents, coordinated through Codex.

Note

Add Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox import to desktop

  • Expands the BROWSER_IMPORT_SOURCES registry to include Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox, using a platform-aware BrowserImportPathContext to resolve user-data directories.
  • Adds a Firefox cookie reader that parses profiles.ini, handles schema-specific expiry and SameSite conversions, filters to default-container cookies, and uses Windows or POSIX lock probes to detect if the browser is running.
  • Extracts shared cookie contracts and SQLite snapshot logic into CookieDatabase.ts for use by both Chromium and Firefox engines.
  • Adds a profileLimitReached failure reason and ProfileLimitReachedError when profile creation or import persistence exceeds the configured maximum.
  • Risk: Chromium sources are now blocked on non-macOS platforms in BrowserImport.unavailableReason; out-of-tree consumers will receive unsupportedPlatform instead of attempting unsupported keychain reads.

Macroscope summarized ff29cce.


Note

Medium Risk
Touches cookie/session import, filesystem path validation, and keychain reads on macOS; Firefox lock probing spawns Python and may allow import while Firefox is running if the probe is unavailable.

Overview
Adds a multi-browser import registry (Chrome, Edge, Brave, Vivaldi, Opera, Arc, Helium, Firefox) with an engine split and BrowserImportPathContext (platform, home, Windows %APPDATA%) replacing the old sourcePaths helper.

Firefox gets a plaintext cookies.sqlite reader, profiles.ini discovery with path hardening, default-container-only imports, schema-aware expiry/SameSite handling, and per-profile running detection (Linux lock symlinks, macOS .parentlock fcntl via a short Python probe, Windows parent.lock). HostProcessAddresses resolves local IPs for lock ownership checks.

Chromium sources share a chromiumSource factory (macOS + Linux paths; non-macOS Chromium import is gated as unsupportedPlatform until other key stores exist). Shared CookieDatabase holds ImportedCookie, WAL-safe snapshotCookieDatabase, and cookieScope. BrowserImport.importCookies branches on engine, skips keychain audit logs for Firefox, and maps Chromium unspecified SameSite to Electron unspecified instead of forcing Lax.

Contracts expand BROWSER_IMPORT_SOURCE_IDS; tests cover Firefox/Chromium readers, snapshots, locks, and discovery edge cases.

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

@coderabbitai

coderabbitaiBot 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: f3c31f7b-c598-4238-adee-9242d001ce25

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-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 16, 2026
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@github-actions

github-actionsBot 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.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.4 KiB15.1 KiB
CodexThread snapshot wire6.9 KiB7.3 KiB
CodexLive turn WebSocket wire6.6 KiB7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB66.4 KiB
CodexLive turn messages1021
ClaudeTotal thread wire13.5 KiB15.1 KiB
ClaudeThread snapshot wire6.9 KiB7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB66.4 KiB
ClaudeLive turn messages1021

Baseline: unavailable · PR result: ff29cce · 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: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

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

@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from bc1215b to 973b17eCompareAugust 16, 2026 22:08
@github-actionsgithub-actionsBot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026

@macroscopeappmacroscopeappBot 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 convention finding: the new Firefox cookie read path models its failure as an anonymous object literal instead of a Schema.TaggedErrorClass. Details inline.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/desktop/src/preview/BrowserImport/BrowserImport.ts
Comment threadapps/desktop/src/preview/BrowserImport/CookieDatabase.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 22:18
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a multi-browser cookie-import capability with new cross-platform discovery, database extraction, lock handling, Keychain interaction, and session-cookie writes. It also introduces a static-analysis diagnostic suppression in a new test, so the scope and sensitive-data handling warrant human review.

No code changes detected at ff29cce. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 973b17e to 1b132e4CompareAugust 16, 2026 22:53
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 3343565 to 56f1705CompareAugust 16, 2026 23:10

@macroscopeappmacroscopeappBot 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: one finding on the new FirefoxCookieReadError. The tagged-error union fix from the previous round looks good; what remains is the structural context the error captures.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 56f1705 to 4d48bd9CompareAugust 16, 2026 23:14
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 4d48bd9 to 0d85caaCompareAugust 16, 2026 23:22
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 7a8cf25 to 27037c9CompareAugust 16, 2026 23:51
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 27037c9 to 9328514CompareAugust 16, 2026 23:59
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
Comment threadapps/desktop/src/preview/BrowserImport/BrowserImport.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated

@cursorcursorBot 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 8360b0f. Configure here.

Comment threadpackages/shared/src/hostProcess.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
juliusmarmingeand others added 22 commits September 2, 2026 16:00
…Firefox
Generalises the importer from one hardcoded browser to a source registry with
two engines.
Chromium forks are table entries: Chrome, Edge, Brave, Vivaldi, Opera, Arc and
Helium all share the existing extractor and differ only in their paths and
keychain coordinates. Those coordinates are pinned per fork rather than
derived, because the forks disagree — Helium uses "Helium Storage Key" /
"Helium" where the others use "<Name> Safe Storage" / "<Name>".
Firefox is a second engine. It stores cookies unencrypted in `cookies.sqlite`,
so there is no key to fetch and no consent prompt — that is Mozilla's design
choice, not a control being circumvented, and it is why Firefox works
identically on all three platforms while Chromium still needs a per-platform
credential store.
Paths resolve for macOS, Windows and Linux from an injected context rather
than from `process`, so a platform's layout can be checked without running on
it. Chromium off macOS still reports `unsupportedPlatform` until those key
stores land; Firefox does not.
The snapshot-before-read step moves to a shared module, since both engines
keep the database open with WAL and must never have the browser's own file
opened for writing.
Firefox has tests against a real `moz_cookies` fixture, including that the
source file is left untouched, and `profiles.ini` parsing covers the
`Install*` sections that name a default profile without describing one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox keeps its lock files inside each profile, not at the user-data root, so
the running check never found them and offered imports from a live, mid-write
database. It now walks the source's profiles and looks for all three names the
platforms use.
Firefox isolates cookies per container and per private window through
`originAttributes`. Electron has no equivalent, so importing them all collapsed
several identities onto one host/name/path and handed the profile whichever
container was written last. Only the default container is imported.
The sidecar copy no longer ignores every error alongside the missing-file case;
the new snapshot test caught that the earlier fix had landed on the
pre-extraction copy of this code rather than the shared module.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Firefox branch failed with an anonymous `{ reason, cause }` literal, and
typing the shared `read` value as that shape erased `ChromiumCookieReadError`'s
tag from the error channel — neither branch could then be handled with
`Effect.catchTags`.
`FirefoxCookieReadError` mirrors its Chromium counterpart, so the union stays
structurally identifiable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox keeps one cookie database per profile, so a failure carrying only a
reason cannot be traced back to the profile that produced it. The path is now a
structural attribute, matching `ChromiumCookieReadError`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox has one failure mode — its plaintext database would not open — so a
single-value `reason` literal encoded the same thing as the tag, and `cause`
was optional though every construction site wraps a real failure. The error now
carries the database path and a required cause, and `BrowserImport` supplies
the user-facing reason where it maps the union.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The union was discriminated with a `cause._tag` ternary inside `Effect.mapError`
even though both members are statically known tagged errors; `Effect.catchTags`
says the same thing without the manual check. The comment above `read` claiming
both carry a `reason` was stale — Firefox's no longer does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox schema 16 (Firefox 129) moved `moz_cookies.expiry` from seconds to
milliseconds — the migration is `UPDATE moz_cookies SET expiry = expiry * 1000`
— but the reader passed the value straight through as seconds, so persistent
cookies from a current Firefox were imported ~1000× too far in the future.
Older profiles still hold seconds, so the unit is decided by `PRAGMA
user_version` rather than assumed either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… browser
`isSourceRunning` treated `.parentlock` / `parent.lock` as proof Firefox held
the profile, but Firefox deliberately leaves those on disk after a clean exit
(as a last-used marker) and only releases the OS-level fcntl or handle lock —
so once Firefox had been used, the source reported `browserRunning` forever and
cookie import never proceeded.
Verified against Firefox's nsProfileLock.cpp: only the Linux `lock` symlink
(target `<ip>:[+]<pid>`) is unlinked on exit, so its presence plus a live pid
is the POSIX signal; a dead pid means a crash. Node has no fcntl probe, so
`.parentlock` carries no signal on POSIX. On Windows the held handle denies our
open as `Busy`, which is the signal there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s detected
Follow-ups to the leftover-lock fix. On macOS a running Firefox writes nothing
but an empty `.parentlock` held with an fcntl lock — no symlink, no pid — so
reading only the Linux `lock` symlink missed it entirely and offered imports
from a live, mid-write database. Node exposes no fcntl, so a throwaway
`python3` child tries a non-blocking F_SETLK and reports whether it blocks;
any failure to get a clean answer counts as held, so a probe problem can
never unlock a live profile. The Linux symlink still short-circuits first.
The symlink's owner half is honoured too: a non-loopback address is a shared
profile locked from another machine, whose pid cannot be probed here, so it
stays held. Also drops a Safari paragraph from `entryExists`' doc that
described `databaseFileExists`' concern, not this helper's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… Windows lock probe
A Firefox import logged "Reading browser cookie key from the keychain" with the executable path even though it never touches a keychain, putting a false security-sensitive event in the audit trail; the log now sits behind the Chromium engine check. Also narrows isLockHeld to the Windows probe its doc describes (its only caller already guards on win32) and deletes entryExists, which that dead branch was the last use of.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ock symlink
The owner check treated only 127.0.0.1 and ::1 as ours, but Firefox writes
whatever its resolver returns for the machine's hostname — 127.0.1.1 on
Debian-style hosts, a LAN address elsewhere, loopback only when the lookup
fails. A crashed Firefox on such a host left a symlink the check read as
foreign and held forever, blocking import. The owner is now matched against
every address the machine answers to, via a new `HostProcessAddresses`
reference that mirrors `HostProcessHostname` and is injectable in tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…name-resolved addresses as local
Two follow-ups on the Firefox lock detection.
A Dock-launched app has launchd's bare PATH, so `python3` could go unfound;
the probe now names `/usr/bin/python3` absolutely first. More importantly,
"held" is now only ever the script's own verdict: a probe that never ran —
no interpreter, or Apple's shim on a Mac without the developer tools, which
exits non-zero after prompting to install — is the probe being unavailable,
not evidence about the lock, and falls back to "not held" rather than
blocking Firefox import on that machine for good. The SQLite snapshot copes
with a live WAL as it does for every other engine.
And `HostProcessAddresses` collected only interface addresses, so a
hostname mapped in /etc/hosts to an address no interface carries — Debian's
127.0.1.1 — read as foreign, holding a crashed Firefox's lock forever. The
set now also includes what the resolver returns for the machine's hostname,
which is exactly what Firefox writes into the lock.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox stores nsICookie::SAMESITE_UNSET (256) for a cookie that carried no
SameSite attribute, distinct from SAMESITE_NONE (0), an explicit opt-in to
cross-site use. The reader collapsed the unknown value onto Lax, and the
reviewed suggestion would have made it None — both change the cookie's
meaning. `ImportedCookie` now carries Electron's own `unspecified`, which lets
the target browser apply its default exactly as the source did, and both
engines map their "no attribute" sentinel (Firefox 256, Chromium -1) to it.
Also drops a doubled `/**` opener left above `cookieDatabaseCandidatePaths`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rows from before schema 9 hold NULL in sameSite, which failed decoding
and aborted the whole import. Schemas 10-14 also keep the declared value
in rawSameSite beside a Lax default; apply Firefox's own schema-15
migration rule so an undeclared cookie imports as unspecified rather
than as an explicit Lax.
The hostname DNS lookup ran once per profile and even on Windows, where
the symlink lock that needs it never exists.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox - #7260

Open
juliusmarminge wants to merge 22 commits into
browser-profile-importfrom
browser-import-more-sources
Open

feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox#7260
juliusmarminge wants to merge 22 commits into
browser-profile-importfrom
browser-import-more-sources

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Stacked on #7255 (browser-profile-import).

Expands cookie import to a source registry with Chromium and Firefox readers.

SourcemacOSWindowsLinux in this layer
Chrome, Edge, Brave, Vivaldi, OperaSupportedUnsupportedDetected; key support in #7261
Arc, HeliumSupportedUnsupportedUnsupported
FirefoxSupportedSupportedSupported

Firefox imports its default container; other containers are excluded so their sessions are not mixed. Discovery respects profiles.ini, validates relative paths, supports explicitly configured absolute profile directories, and ignores fallback entries without a cookie database. Counts use the same default-container filter as the reader.

Both engines share a consistent read-only SQLite snapshot path. Windows Chromium remains intentionally unsupported, preserving the Windows-tested contribution #7323.

Validation: focused source-discovery, path-validation, Firefox cookie/count, and SQLite snapshot fixtures. Cross-platform path tests run on macOS; they are not a substitute for another live Windows/Linux test.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents, coordinated through Codex.

Note

Add Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox import to desktop

  • Expands the BROWSER_IMPORT_SOURCES registry to include Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox, using a platform-aware BrowserImportPathContext to resolve user-data directories.
  • Adds a Firefox cookie reader that parses profiles.ini, handles schema-specific expiry and SameSite conversions, filters to default-container cookies, and uses Windows or POSIX lock probes to detect if the browser is running.
  • Extracts shared cookie contracts and SQLite snapshot logic into CookieDatabase.ts for use by both Chromium and Firefox engines.
  • Adds a profileLimitReached failure reason and ProfileLimitReachedError when profile creation or import persistence exceeds the configured maximum.
  • Risk: Chromium sources are now blocked on non-macOS platforms in BrowserImport.unavailableReason; out-of-tree consumers will receive unsupportedPlatform instead of attempting unsupported keychain reads.

Macroscope summarized ff29cce.


Note

Medium Risk
Touches cookie/session import, filesystem path validation, and keychain reads on macOS; Firefox lock probing spawns Python and may allow import while Firefox is running if the probe is unavailable.

Overview
Adds a multi-browser import registry (Chrome, Edge, Brave, Vivaldi, Opera, Arc, Helium, Firefox) with an engine split and BrowserImportPathContext (platform, home, Windows %APPDATA%) replacing the old sourcePaths helper.

Firefox gets a plaintext cookies.sqlite reader, profiles.ini discovery with path hardening, default-container-only imports, schema-aware expiry/SameSite handling, and per-profile running detection (Linux lock symlinks, macOS .parentlock fcntl via a short Python probe, Windows parent.lock). HostProcessAddresses resolves local IPs for lock ownership checks.

Chromium sources share a chromiumSource factory (macOS + Linux paths; non-macOS Chromium import is gated as unsupportedPlatform until other key stores exist). Shared CookieDatabase holds ImportedCookie, WAL-safe snapshotCookieDatabase, and cookieScope. BrowserImport.importCookies branches on engine, skips keychain audit logs for Firefox, and maps Chromium unspecified SameSite to Electron unspecified instead of forcing Lax.

Contracts expand BROWSER_IMPORT_SOURCE_IDS; tests cover Firefox/Chromium readers, snapshots, locks, and discovery edge cases.

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

@coderabbitai

coderabbitaiBot 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: f3c31f7b-c598-4238-adee-9242d001ce25

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-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 16, 2026
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@github-actions

github-actionsBot 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.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.4 KiB15.1 KiB
CodexThread snapshot wire6.9 KiB7.3 KiB
CodexLive turn WebSocket wire6.6 KiB7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB66.4 KiB
CodexLive turn messages1021
ClaudeTotal thread wire13.5 KiB15.1 KiB
ClaudeThread snapshot wire6.9 KiB7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB66.4 KiB
ClaudeLive turn messages1021

Baseline: unavailable · PR result: ff29cce · 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: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

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

@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from bc1215b to 973b17eCompareAugust 16, 2026 22:08
@github-actionsgithub-actionsBot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026

@macroscopeappmacroscopeappBot 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 convention finding: the new Firefox cookie read path models its failure as an anonymous object literal instead of a Schema.TaggedErrorClass. Details inline.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/desktop/src/preview/BrowserImport/BrowserImport.ts
Comment threadapps/desktop/src/preview/BrowserImport/CookieDatabase.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 22:18
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a multi-browser cookie-import capability with new cross-platform discovery, database extraction, lock handling, Keychain interaction, and session-cookie writes. It also introduces a static-analysis diagnostic suppression in a new test, so the scope and sensitive-data handling warrant human review.

No code changes detected at ff29cce. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 973b17e to 1b132e4CompareAugust 16, 2026 22:53
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 3343565 to 56f1705CompareAugust 16, 2026 23:10

@macroscopeappmacroscopeappBot 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: one finding on the new FirefoxCookieReadError. The tagged-error union fix from the previous round looks good; what remains is the structural context the error captures.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 56f1705 to 4d48bd9CompareAugust 16, 2026 23:14
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 4d48bd9 to 0d85caaCompareAugust 16, 2026 23:22
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 7a8cf25 to 27037c9CompareAugust 16, 2026 23:51
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 27037c9 to 9328514CompareAugust 16, 2026 23:59
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
Comment threadapps/desktop/src/preview/BrowserImport/BrowserImport.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated

@cursorcursorBot 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 8360b0f. Configure here.

Comment threadpackages/shared/src/hostProcess.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
juliusmarmingeand others added 22 commits September 2, 2026 16:00
…Firefox
Generalises the importer from one hardcoded browser to a source registry with
two engines.
Chromium forks are table entries: Chrome, Edge, Brave, Vivaldi, Opera, Arc and
Helium all share the existing extractor and differ only in their paths and
keychain coordinates. Those coordinates are pinned per fork rather than
derived, because the forks disagree — Helium uses "Helium Storage Key" /
"Helium" where the others use "<Name> Safe Storage" / "<Name>".
Firefox is a second engine. It stores cookies unencrypted in `cookies.sqlite`,
so there is no key to fetch and no consent prompt — that is Mozilla's design
choice, not a control being circumvented, and it is why Firefox works
identically on all three platforms while Chromium still needs a per-platform
credential store.
Paths resolve for macOS, Windows and Linux from an injected context rather
than from `process`, so a platform's layout can be checked without running on
it. Chromium off macOS still reports `unsupportedPlatform` until those key
stores land; Firefox does not.
The snapshot-before-read step moves to a shared module, since both engines
keep the database open with WAL and must never have the browser's own file
opened for writing.
Firefox has tests against a real `moz_cookies` fixture, including that the
source file is left untouched, and `profiles.ini` parsing covers the
`Install*` sections that name a default profile without describing one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox keeps its lock files inside each profile, not at the user-data root, so
the running check never found them and offered imports from a live, mid-write
database. It now walks the source's profiles and looks for all three names the
platforms use.
Firefox isolates cookies per container and per private window through
`originAttributes`. Electron has no equivalent, so importing them all collapsed
several identities onto one host/name/path and handed the profile whichever
container was written last. Only the default container is imported.
The sidecar copy no longer ignores every error alongside the missing-file case;
the new snapshot test caught that the earlier fix had landed on the
pre-extraction copy of this code rather than the shared module.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Firefox branch failed with an anonymous `{ reason, cause }` literal, and
typing the shared `read` value as that shape erased `ChromiumCookieReadError`'s
tag from the error channel — neither branch could then be handled with
`Effect.catchTags`.
`FirefoxCookieReadError` mirrors its Chromium counterpart, so the union stays
structurally identifiable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox keeps one cookie database per profile, so a failure carrying only a
reason cannot be traced back to the profile that produced it. The path is now a
structural attribute, matching `ChromiumCookieReadError`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox has one failure mode — its plaintext database would not open — so a
single-value `reason` literal encoded the same thing as the tag, and `cause`
was optional though every construction site wraps a real failure. The error now
carries the database path and a required cause, and `BrowserImport` supplies
the user-facing reason where it maps the union.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The union was discriminated with a `cause._tag` ternary inside `Effect.mapError`
even though both members are statically known tagged errors; `Effect.catchTags`
says the same thing without the manual check. The comment above `read` claiming
both carry a `reason` was stale — Firefox's no longer does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox schema 16 (Firefox 129) moved `moz_cookies.expiry` from seconds to
milliseconds — the migration is `UPDATE moz_cookies SET expiry = expiry * 1000`
— but the reader passed the value straight through as seconds, so persistent
cookies from a current Firefox were imported ~1000× too far in the future.
Older profiles still hold seconds, so the unit is decided by `PRAGMA
user_version` rather than assumed either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… browser
`isSourceRunning` treated `.parentlock` / `parent.lock` as proof Firefox held
the profile, but Firefox deliberately leaves those on disk after a clean exit
(as a last-used marker) and only releases the OS-level fcntl or handle lock —
so once Firefox had been used, the source reported `browserRunning` forever and
cookie import never proceeded.
Verified against Firefox's nsProfileLock.cpp: only the Linux `lock` symlink
(target `<ip>:[+]<pid>`) is unlinked on exit, so its presence plus a live pid
is the POSIX signal; a dead pid means a crash. Node has no fcntl probe, so
`.parentlock` carries no signal on POSIX. On Windows the held handle denies our
open as `Busy`, which is the signal there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s detected
Follow-ups to the leftover-lock fix. On macOS a running Firefox writes nothing
but an empty `.parentlock` held with an fcntl lock — no symlink, no pid — so
reading only the Linux `lock` symlink missed it entirely and offered imports
from a live, mid-write database. Node exposes no fcntl, so a throwaway
`python3` child tries a non-blocking F_SETLK and reports whether it blocks;
any failure to get a clean answer counts as held, so a probe problem can
never unlock a live profile. The Linux symlink still short-circuits first.
The symlink's owner half is honoured too: a non-loopback address is a shared
profile locked from another machine, whose pid cannot be probed here, so it
stays held. Also drops a Safari paragraph from `entryExists`' doc that
described `databaseFileExists`' concern, not this helper's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… Windows lock probe
A Firefox import logged "Reading browser cookie key from the keychain" with the executable path even though it never touches a keychain, putting a false security-sensitive event in the audit trail; the log now sits behind the Chromium engine check. Also narrows isLockHeld to the Windows probe its doc describes (its only caller already guards on win32) and deletes entryExists, which that dead branch was the last use of.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ock symlink
The owner check treated only 127.0.0.1 and ::1 as ours, but Firefox writes
whatever its resolver returns for the machine's hostname — 127.0.1.1 on
Debian-style hosts, a LAN address elsewhere, loopback only when the lookup
fails. A crashed Firefox on such a host left a symlink the check read as
foreign and held forever, blocking import. The owner is now matched against
every address the machine answers to, via a new `HostProcessAddresses`
reference that mirrors `HostProcessHostname` and is injectable in tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…name-resolved addresses as local
Two follow-ups on the Firefox lock detection.
A Dock-launched app has launchd's bare PATH, so `python3` could go unfound;
the probe now names `/usr/bin/python3` absolutely first. More importantly,
"held" is now only ever the script's own verdict: a probe that never ran —
no interpreter, or Apple's shim on a Mac without the developer tools, which
exits non-zero after prompting to install — is the probe being unavailable,
not evidence about the lock, and falls back to "not held" rather than
blocking Firefox import on that machine for good. The SQLite snapshot copes
with a live WAL as it does for every other engine.
And `HostProcessAddresses` collected only interface addresses, so a
hostname mapped in /etc/hosts to an address no interface carries — Debian's
127.0.1.1 — read as foreign, holding a crashed Firefox's lock forever. The
set now also includes what the resolver returns for the machine's hostname,
which is exactly what Firefox writes into the lock.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox stores nsICookie::SAMESITE_UNSET (256) for a cookie that carried no
SameSite attribute, distinct from SAMESITE_NONE (0), an explicit opt-in to
cross-site use. The reader collapsed the unknown value onto Lax, and the
reviewed suggestion would have made it None — both change the cookie's
meaning. `ImportedCookie` now carries Electron's own `unspecified`, which lets
the target browser apply its default exactly as the source did, and both
engines map their "no attribute" sentinel (Firefox 256, Chromium -1) to it.
Also drops a doubled `/**` opener left above `cookieDatabaseCandidatePaths`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rows from before schema 9 hold NULL in sameSite, which failed decoding
and aborted the whole import. Schemas 10-14 also keep the declared value
in rawSameSite beside a Lax default; apply Firefox's own schema-15
migration rule so an undeclared cookie imports as unspecified rather
than as an explicit Lax.
The hostname DNS lookup ran once per profile and even on Windows, where
the symlink lock that needs it never exists.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox - #7260

Open
juliusmarminge wants to merge 22 commits into
browser-profile-importfrom
browser-import-more-sources
Open

feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox#7260
juliusmarminge wants to merge 22 commits into
browser-profile-importfrom
browser-import-more-sources

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Stacked on #7255 (browser-profile-import).

Expands cookie import to a source registry with Chromium and Firefox readers.

SourcemacOSWindowsLinux in this layer
Chrome, Edge, Brave, Vivaldi, OperaSupportedUnsupportedDetected; key support in #7261
Arc, HeliumSupportedUnsupportedUnsupported
FirefoxSupportedSupportedSupported

Firefox imports its default container; other containers are excluded so their sessions are not mixed. Discovery respects profiles.ini, validates relative paths, supports explicitly configured absolute profile directories, and ignores fallback entries without a cookie database. Counts use the same default-container filter as the reader.

Both engines share a consistent read-only SQLite snapshot path. Windows Chromium remains intentionally unsupported, preserving the Windows-tested contribution #7323.

Validation: focused source-discovery, path-validation, Firefox cookie/count, and SQLite snapshot fixtures. Cross-platform path tests run on macOS; they are not a substitute for another live Windows/Linux test.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents, coordinated through Codex.

Note

Add Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox import to desktop

  • Expands the BROWSER_IMPORT_SOURCES registry to include Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox, using a platform-aware BrowserImportPathContext to resolve user-data directories.
  • Adds a Firefox cookie reader that parses profiles.ini, handles schema-specific expiry and SameSite conversions, filters to default-container cookies, and uses Windows or POSIX lock probes to detect if the browser is running.
  • Extracts shared cookie contracts and SQLite snapshot logic into CookieDatabase.ts for use by both Chromium and Firefox engines.
  • Adds a profileLimitReached failure reason and ProfileLimitReachedError when profile creation or import persistence exceeds the configured maximum.
  • Risk: Chromium sources are now blocked on non-macOS platforms in BrowserImport.unavailableReason; out-of-tree consumers will receive unsupportedPlatform instead of attempting unsupported keychain reads.

Macroscope summarized ff29cce.


Note

Medium Risk
Touches cookie/session import, filesystem path validation, and keychain reads on macOS; Firefox lock probing spawns Python and may allow import while Firefox is running if the probe is unavailable.

Overview
Adds a multi-browser import registry (Chrome, Edge, Brave, Vivaldi, Opera, Arc, Helium, Firefox) with an engine split and BrowserImportPathContext (platform, home, Windows %APPDATA%) replacing the old sourcePaths helper.

Firefox gets a plaintext cookies.sqlite reader, profiles.ini discovery with path hardening, default-container-only imports, schema-aware expiry/SameSite handling, and per-profile running detection (Linux lock symlinks, macOS .parentlock fcntl via a short Python probe, Windows parent.lock). HostProcessAddresses resolves local IPs for lock ownership checks.

Chromium sources share a chromiumSource factory (macOS + Linux paths; non-macOS Chromium import is gated as unsupportedPlatform until other key stores exist). Shared CookieDatabase holds ImportedCookie, WAL-safe snapshotCookieDatabase, and cookieScope. BrowserImport.importCookies branches on engine, skips keychain audit logs for Firefox, and maps Chromium unspecified SameSite to Electron unspecified instead of forcing Lax.

Contracts expand BROWSER_IMPORT_SOURCE_IDS; tests cover Firefox/Chromium readers, snapshots, locks, and discovery edge cases.

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

@coderabbitai

coderabbitaiBot 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: f3c31f7b-c598-4238-adee-9242d001ce25

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-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 16, 2026
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@github-actions

github-actionsBot 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.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.4 KiB15.1 KiB
CodexThread snapshot wire6.9 KiB7.3 KiB
CodexLive turn WebSocket wire6.6 KiB7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB66.4 KiB
CodexLive turn messages1021
ClaudeTotal thread wire13.5 KiB15.1 KiB
ClaudeThread snapshot wire6.9 KiB7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB66.4 KiB
ClaudeLive turn messages1021

Baseline: unavailable · PR result: ff29cce · 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: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

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

@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from bc1215b to 973b17eCompareAugust 16, 2026 22:08
@github-actionsgithub-actionsBot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026

@macroscopeappmacroscopeappBot 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 convention finding: the new Firefox cookie read path models its failure as an anonymous object literal instead of a Schema.TaggedErrorClass. Details inline.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/desktop/src/preview/BrowserImport/BrowserImport.ts
Comment threadapps/desktop/src/preview/BrowserImport/CookieDatabase.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 22:18
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a multi-browser cookie-import capability with new cross-platform discovery, database extraction, lock handling, Keychain interaction, and session-cookie writes. It also introduces a static-analysis diagnostic suppression in a new test, so the scope and sensitive-data handling warrant human review.

No code changes detected at ff29cce. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 973b17e to 1b132e4CompareAugust 16, 2026 22:53
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 3343565 to 56f1705CompareAugust 16, 2026 23:10

@macroscopeappmacroscopeappBot 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: one finding on the new FirefoxCookieReadError. The tagged-error union fix from the previous round looks good; what remains is the structural context the error captures.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 56f1705 to 4d48bd9CompareAugust 16, 2026 23:14
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 4d48bd9 to 0d85caaCompareAugust 16, 2026 23:22
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 7a8cf25 to 27037c9CompareAugust 16, 2026 23:51
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 27037c9 to 9328514CompareAugust 16, 2026 23:59
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
Comment threadapps/desktop/src/preview/BrowserImport/BrowserImport.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated

@cursorcursorBot 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 8360b0f. Configure here.

Comment threadpackages/shared/src/hostProcess.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
juliusmarmingeand others added 22 commits September 2, 2026 16:00
…Firefox
Generalises the importer from one hardcoded browser to a source registry with
two engines.
Chromium forks are table entries: Chrome, Edge, Brave, Vivaldi, Opera, Arc and
Helium all share the existing extractor and differ only in their paths and
keychain coordinates. Those coordinates are pinned per fork rather than
derived, because the forks disagree — Helium uses "Helium Storage Key" /
"Helium" where the others use "<Name> Safe Storage" / "<Name>".
Firefox is a second engine. It stores cookies unencrypted in `cookies.sqlite`,
so there is no key to fetch and no consent prompt — that is Mozilla's design
choice, not a control being circumvented, and it is why Firefox works
identically on all three platforms while Chromium still needs a per-platform
credential store.
Paths resolve for macOS, Windows and Linux from an injected context rather
than from `process`, so a platform's layout can be checked without running on
it. Chromium off macOS still reports `unsupportedPlatform` until those key
stores land; Firefox does not.
The snapshot-before-read step moves to a shared module, since both engines
keep the database open with WAL and must never have the browser's own file
opened for writing.
Firefox has tests against a real `moz_cookies` fixture, including that the
source file is left untouched, and `profiles.ini` parsing covers the
`Install*` sections that name a default profile without describing one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox keeps its lock files inside each profile, not at the user-data root, so
the running check never found them and offered imports from a live, mid-write
database. It now walks the source's profiles and looks for all three names the
platforms use.
Firefox isolates cookies per container and per private window through
`originAttributes`. Electron has no equivalent, so importing them all collapsed
several identities onto one host/name/path and handed the profile whichever
container was written last. Only the default container is imported.
The sidecar copy no longer ignores every error alongside the missing-file case;
the new snapshot test caught that the earlier fix had landed on the
pre-extraction copy of this code rather than the shared module.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Firefox branch failed with an anonymous `{ reason, cause }` literal, and
typing the shared `read` value as that shape erased `ChromiumCookieReadError`'s
tag from the error channel — neither branch could then be handled with
`Effect.catchTags`.
`FirefoxCookieReadError` mirrors its Chromium counterpart, so the union stays
structurally identifiable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox keeps one cookie database per profile, so a failure carrying only a
reason cannot be traced back to the profile that produced it. The path is now a
structural attribute, matching `ChromiumCookieReadError`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox has one failure mode — its plaintext database would not open — so a
single-value `reason` literal encoded the same thing as the tag, and `cause`
was optional though every construction site wraps a real failure. The error now
carries the database path and a required cause, and `BrowserImport` supplies
the user-facing reason where it maps the union.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The union was discriminated with a `cause._tag` ternary inside `Effect.mapError`
even though both members are statically known tagged errors; `Effect.catchTags`
says the same thing without the manual check. The comment above `read` claiming
both carry a `reason` was stale — Firefox's no longer does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox schema 16 (Firefox 129) moved `moz_cookies.expiry` from seconds to
milliseconds — the migration is `UPDATE moz_cookies SET expiry = expiry * 1000`
— but the reader passed the value straight through as seconds, so persistent
cookies from a current Firefox were imported ~1000× too far in the future.
Older profiles still hold seconds, so the unit is decided by `PRAGMA
user_version` rather than assumed either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… browser
`isSourceRunning` treated `.parentlock` / `parent.lock` as proof Firefox held
the profile, but Firefox deliberately leaves those on disk after a clean exit
(as a last-used marker) and only releases the OS-level fcntl or handle lock —
so once Firefox had been used, the source reported `browserRunning` forever and
cookie import never proceeded.
Verified against Firefox's nsProfileLock.cpp: only the Linux `lock` symlink
(target `<ip>:[+]<pid>`) is unlinked on exit, so its presence plus a live pid
is the POSIX signal; a dead pid means a crash. Node has no fcntl probe, so
`.parentlock` carries no signal on POSIX. On Windows the held handle denies our
open as `Busy`, which is the signal there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s detected
Follow-ups to the leftover-lock fix. On macOS a running Firefox writes nothing
but an empty `.parentlock` held with an fcntl lock — no symlink, no pid — so
reading only the Linux `lock` symlink missed it entirely and offered imports
from a live, mid-write database. Node exposes no fcntl, so a throwaway
`python3` child tries a non-blocking F_SETLK and reports whether it blocks;
any failure to get a clean answer counts as held, so a probe problem can
never unlock a live profile. The Linux symlink still short-circuits first.
The symlink's owner half is honoured too: a non-loopback address is a shared
profile locked from another machine, whose pid cannot be probed here, so it
stays held. Also drops a Safari paragraph from `entryExists`' doc that
described `databaseFileExists`' concern, not this helper's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… Windows lock probe
A Firefox import logged "Reading browser cookie key from the keychain" with the executable path even though it never touches a keychain, putting a false security-sensitive event in the audit trail; the log now sits behind the Chromium engine check. Also narrows isLockHeld to the Windows probe its doc describes (its only caller already guards on win32) and deletes entryExists, which that dead branch was the last use of.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ock symlink
The owner check treated only 127.0.0.1 and ::1 as ours, but Firefox writes
whatever its resolver returns for the machine's hostname — 127.0.1.1 on
Debian-style hosts, a LAN address elsewhere, loopback only when the lookup
fails. A crashed Firefox on such a host left a symlink the check read as
foreign and held forever, blocking import. The owner is now matched against
every address the machine answers to, via a new `HostProcessAddresses`
reference that mirrors `HostProcessHostname` and is injectable in tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…name-resolved addresses as local
Two follow-ups on the Firefox lock detection.
A Dock-launched app has launchd's bare PATH, so `python3` could go unfound;
the probe now names `/usr/bin/python3` absolutely first. More importantly,
"held" is now only ever the script's own verdict: a probe that never ran —
no interpreter, or Apple's shim on a Mac without the developer tools, which
exits non-zero after prompting to install — is the probe being unavailable,
not evidence about the lock, and falls back to "not held" rather than
blocking Firefox import on that machine for good. The SQLite snapshot copes
with a live WAL as it does for every other engine.
And `HostProcessAddresses` collected only interface addresses, so a
hostname mapped in /etc/hosts to an address no interface carries — Debian's
127.0.1.1 — read as foreign, holding a crashed Firefox's lock forever. The
set now also includes what the resolver returns for the machine's hostname,
which is exactly what Firefox writes into the lock.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox stores nsICookie::SAMESITE_UNSET (256) for a cookie that carried no
SameSite attribute, distinct from SAMESITE_NONE (0), an explicit opt-in to
cross-site use. The reader collapsed the unknown value onto Lax, and the
reviewed suggestion would have made it None — both change the cookie's
meaning. `ImportedCookie` now carries Electron's own `unspecified`, which lets
the target browser apply its default exactly as the source did, and both
engines map their "no attribute" sentinel (Firefox 256, Chromium -1) to it.
Also drops a doubled `/**` opener left above `cookieDatabaseCandidatePaths`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rows from before schema 9 hold NULL in sameSite, which failed decoding
and aborted the whole import. Schemas 10-14 also keep the declared value
in rawSameSite beside a Lax default; apply Firefox's own schema-15
migration rule so an undeclared cookie imports as unspecified rather
than as an explicit Lax.
The hostname DNS lookup ran once per profile and even on Windows, where
the symlink lock that needs it never exists.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox - #7260

Open
juliusmarminge wants to merge 22 commits into
browser-profile-importfrom
browser-import-more-sources
Open

feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox#7260
juliusmarminge wants to merge 22 commits into
browser-profile-importfrom
browser-import-more-sources

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Stacked on #7255 (browser-profile-import).

Expands cookie import to a source registry with Chromium and Firefox readers.

SourcemacOSWindowsLinux in this layer
Chrome, Edge, Brave, Vivaldi, OperaSupportedUnsupportedDetected; key support in #7261
Arc, HeliumSupportedUnsupportedUnsupported
FirefoxSupportedSupportedSupported

Firefox imports its default container; other containers are excluded so their sessions are not mixed. Discovery respects profiles.ini, validates relative paths, supports explicitly configured absolute profile directories, and ignores fallback entries without a cookie database. Counts use the same default-container filter as the reader.

Both engines share a consistent read-only SQLite snapshot path. Windows Chromium remains intentionally unsupported, preserving the Windows-tested contribution #7323.

Validation: focused source-discovery, path-validation, Firefox cookie/count, and SQLite snapshot fixtures. Cross-platform path tests run on macOS; they are not a substitute for another live Windows/Linux test.

Original implementation: Claude Code. Review fixes: GPT-5.6 Sol agents, coordinated through Codex.

Note

Add Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox import to desktop

  • Expands the BROWSER_IMPORT_SOURCES registry to include Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox, using a platform-aware BrowserImportPathContext to resolve user-data directories.
  • Adds a Firefox cookie reader that parses profiles.ini, handles schema-specific expiry and SameSite conversions, filters to default-container cookies, and uses Windows or POSIX lock probes to detect if the browser is running.
  • Extracts shared cookie contracts and SQLite snapshot logic into CookieDatabase.ts for use by both Chromium and Firefox engines.
  • Adds a profileLimitReached failure reason and ProfileLimitReachedError when profile creation or import persistence exceeds the configured maximum.
  • Risk: Chromium sources are now blocked on non-macOS platforms in BrowserImport.unavailableReason; out-of-tree consumers will receive unsupportedPlatform instead of attempting unsupported keychain reads.

Macroscope summarized ff29cce.


Note

Medium Risk
Touches cookie/session import, filesystem path validation, and keychain reads on macOS; Firefox lock probing spawns Python and may allow import while Firefox is running if the probe is unavailable.

Overview
Adds a multi-browser import registry (Chrome, Edge, Brave, Vivaldi, Opera, Arc, Helium, Firefox) with an engine split and BrowserImportPathContext (platform, home, Windows %APPDATA%) replacing the old sourcePaths helper.

Firefox gets a plaintext cookies.sqlite reader, profiles.ini discovery with path hardening, default-container-only imports, schema-aware expiry/SameSite handling, and per-profile running detection (Linux lock symlinks, macOS .parentlock fcntl via a short Python probe, Windows parent.lock). HostProcessAddresses resolves local IPs for lock ownership checks.

Chromium sources share a chromiumSource factory (macOS + Linux paths; non-macOS Chromium import is gated as unsupportedPlatform until other key stores exist). Shared CookieDatabase holds ImportedCookie, WAL-safe snapshotCookieDatabase, and cookieScope. BrowserImport.importCookies branches on engine, skips keychain audit logs for Firefox, and maps Chromium unspecified SameSite to Electron unspecified instead of forcing Lax.

Contracts expand BROWSER_IMPORT_SOURCE_IDS; tests cover Firefox/Chromium readers, snapshots, locks, and discovery edge cases.

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

@coderabbitai

coderabbitaiBot 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: f3c31f7b-c598-4238-adee-9242d001ce25

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-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 16, 2026
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@github-actions

github-actionsBot 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.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.4 KiB15.1 KiB
CodexThread snapshot wire6.9 KiB7.3 KiB
CodexLive turn WebSocket wire6.6 KiB7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB66.4 KiB
CodexLive turn messages1021
ClaudeTotal thread wire13.5 KiB15.1 KiB
ClaudeThread snapshot wire6.9 KiB7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB66.4 KiB
ClaudeLive turn messages1021

Baseline: unavailable · PR result: ff29cce · 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: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

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

@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from bc1215b to 973b17eCompareAugust 16, 2026 22:08
@github-actionsgithub-actionsBot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026

@macroscopeappmacroscopeappBot 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 convention finding: the new Firefox cookie read path models its failure as an anonymous object literal instead of a Schema.TaggedErrorClass. Details inline.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/desktop/src/preview/BrowserImport/BrowserImport.ts
Comment threadapps/desktop/src/preview/BrowserImport/CookieDatabase.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 22:18
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a multi-browser cookie-import capability with new cross-platform discovery, database extraction, lock handling, Keychain interaction, and session-cookie writes. It also introduces a static-analysis diagnostic suppression in a new test, so the scope and sensitive-data handling warrant human review.

No code changes detected at ff29cce. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 973b17e to 1b132e4CompareAugust 16, 2026 22:53
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 3343565 to 56f1705CompareAugust 16, 2026 23:10

@macroscopeappmacroscopeappBot 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: one finding on the new FirefoxCookieReadError. The tagged-error union fix from the previous round looks good; what remains is the structural context the error captures.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 56f1705 to 4d48bd9CompareAugust 16, 2026 23:14
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 4d48bd9 to 0d85caaCompareAugust 16, 2026 23:22
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 7a8cf25 to 27037c9CompareAugust 16, 2026 23:51
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts
@juliusmarminge
juliusmarmingeforce-pushed the browser-import-more-sources branch from 27037c9 to 9328514CompareAugust 16, 2026 23:59
@github-actionsgithub-actionsBot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
Comment threadapps/desktop/src/preview/BrowserImport/BrowserImport.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated

@cursorcursorBot 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 8360b0f. Configure here.

Comment threadpackages/shared/src/hostProcess.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
Comment threadapps/desktop/src/preview/BrowserImport/Sources.ts Outdated
Comment threadapps/desktop/src/preview/BrowserImport/FirefoxCookies.ts
juliusmarmingeand others added 22 commits September 2, 2026 16:00
…Firefox
Generalises the importer from one hardcoded browser to a source registry with
two engines.
Chromium forks are table entries: Chrome, Edge, Brave, Vivaldi, Opera, Arc and
Helium all share the existing extractor and differ only in their paths and
keychain coordinates. Those coordinates are pinned per fork rather than
derived, because the forks disagree — Helium uses "Helium Storage Key" /
"Helium" where the others use "<Name> Safe Storage" / "<Name>".
Firefox is a second engine. It stores cookies unencrypted in `cookies.sqlite`,
so there is no key to fetch and no consent prompt — that is Mozilla's design
choice, not a control being circumvented, and it is why Firefox works
identically on all three platforms while Chromium still needs a per-platform
credential store.
Paths resolve for macOS, Windows and Linux from an injected context rather
than from `process`, so a platform's layout can be checked without running on
it. Chromium off macOS still reports `unsupportedPlatform` until those key
stores land; Firefox does not.
The snapshot-before-read step moves to a shared module, since both engines
keep the database open with WAL and must never have the browser's own file
opened for writing.
Firefox has tests against a real `moz_cookies` fixture, including that the
source file is left untouched, and `profiles.ini` parsing covers the
`Install*` sections that name a default profile without describing one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox keeps its lock files inside each profile, not at the user-data root, so
the running check never found them and offered imports from a live, mid-write
database. It now walks the source's profiles and looks for all three names the
platforms use.
Firefox isolates cookies per container and per private window through
`originAttributes`. Electron has no equivalent, so importing them all collapsed
several identities onto one host/name/path and handed the profile whichever
container was written last. Only the default container is imported.
The sidecar copy no longer ignores every error alongside the missing-file case;
the new snapshot test caught that the earlier fix had landed on the
pre-extraction copy of this code rather than the shared module.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Firefox branch failed with an anonymous `{ reason, cause }` literal, and
typing the shared `read` value as that shape erased `ChromiumCookieReadError`'s
tag from the error channel — neither branch could then be handled with
`Effect.catchTags`.
`FirefoxCookieReadError` mirrors its Chromium counterpart, so the union stays
structurally identifiable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox keeps one cookie database per profile, so a failure carrying only a
reason cannot be traced back to the profile that produced it. The path is now a
structural attribute, matching `ChromiumCookieReadError`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox has one failure mode — its plaintext database would not open — so a
single-value `reason` literal encoded the same thing as the tag, and `cause`
was optional though every construction site wraps a real failure. The error now
carries the database path and a required cause, and `BrowserImport` supplies
the user-facing reason where it maps the union.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The union was discriminated with a `cause._tag` ternary inside `Effect.mapError`
even though both members are statically known tagged errors; `Effect.catchTags`
says the same thing without the manual check. The comment above `read` claiming
both carry a `reason` was stale — Firefox's no longer does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox schema 16 (Firefox 129) moved `moz_cookies.expiry` from seconds to
milliseconds — the migration is `UPDATE moz_cookies SET expiry = expiry * 1000`
— but the reader passed the value straight through as seconds, so persistent
cookies from a current Firefox were imported ~1000× too far in the future.
Older profiles still hold seconds, so the unit is decided by `PRAGMA
user_version` rather than assumed either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… browser
`isSourceRunning` treated `.parentlock` / `parent.lock` as proof Firefox held
the profile, but Firefox deliberately leaves those on disk after a clean exit
(as a last-used marker) and only releases the OS-level fcntl or handle lock —
so once Firefox had been used, the source reported `browserRunning` forever and
cookie import never proceeded.
Verified against Firefox's nsProfileLock.cpp: only the Linux `lock` symlink
(target `<ip>:[+]<pid>`) is unlinked on exit, so its presence plus a live pid
is the POSIX signal; a dead pid means a crash. Node has no fcntl probe, so
`.parentlock` carries no signal on POSIX. On Windows the held handle denies our
open as `Busy`, which is the signal there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s detected
Follow-ups to the leftover-lock fix. On macOS a running Firefox writes nothing
but an empty `.parentlock` held with an fcntl lock — no symlink, no pid — so
reading only the Linux `lock` symlink missed it entirely and offered imports
from a live, mid-write database. Node exposes no fcntl, so a throwaway
`python3` child tries a non-blocking F_SETLK and reports whether it blocks;
any failure to get a clean answer counts as held, so a probe problem can
never unlock a live profile. The Linux symlink still short-circuits first.
The symlink's owner half is honoured too: a non-loopback address is a shared
profile locked from another machine, whose pid cannot be probed here, so it
stays held. Also drops a Safari paragraph from `entryExists`' doc that
described `databaseFileExists`' concern, not this helper's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… Windows lock probe
A Firefox import logged "Reading browser cookie key from the keychain" with the executable path even though it never touches a keychain, putting a false security-sensitive event in the audit trail; the log now sits behind the Chromium engine check. Also narrows isLockHeld to the Windows probe its doc describes (its only caller already guards on win32) and deletes entryExists, which that dead branch was the last use of.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ock symlink
The owner check treated only 127.0.0.1 and ::1 as ours, but Firefox writes
whatever its resolver returns for the machine's hostname — 127.0.1.1 on
Debian-style hosts, a LAN address elsewhere, loopback only when the lookup
fails. A crashed Firefox on such a host left a symlink the check read as
foreign and held forever, blocking import. The owner is now matched against
every address the machine answers to, via a new `HostProcessAddresses`
reference that mirrors `HostProcessHostname` and is injectable in tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…name-resolved addresses as local
Two follow-ups on the Firefox lock detection.
A Dock-launched app has launchd's bare PATH, so `python3` could go unfound;
the probe now names `/usr/bin/python3` absolutely first. More importantly,
"held" is now only ever the script's own verdict: a probe that never ran —
no interpreter, or Apple's shim on a Mac without the developer tools, which
exits non-zero after prompting to install — is the probe being unavailable,
not evidence about the lock, and falls back to "not held" rather than
blocking Firefox import on that machine for good. The SQLite snapshot copes
with a live WAL as it does for every other engine.
And `HostProcessAddresses` collected only interface addresses, so a
hostname mapped in /etc/hosts to an address no interface carries — Debian's
127.0.1.1 — read as foreign, holding a crashed Firefox's lock forever. The
set now also includes what the resolver returns for the machine's hostname,
which is exactly what Firefox writes into the lock.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox stores nsICookie::SAMESITE_UNSET (256) for a cookie that carried no
SameSite attribute, distinct from SAMESITE_NONE (0), an explicit opt-in to
cross-site use. The reader collapsed the unknown value onto Lax, and the
reviewed suggestion would have made it None — both change the cookie's
meaning. `ImportedCookie` now carries Electron's own `unspecified`, which lets
the target browser apply its default exactly as the source did, and both
engines map their "no attribute" sentinel (Firefox 256, Chromium -1) to it.
Also drops a doubled `/**` opener left above `cookieDatabaseCandidatePaths`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rows from before schema 9 hold NULL in sameSite, which failed decoding
and aborted the whole import. Schemas 10-14 also keep the declared value
in rawSameSite beside a Lax default; apply Firefox's own schema-15
migration rule so an undeclared cookie imports as unspecified rather
than as an explicit Lax.
The hostname DNS lookup ran once per profile and even on Windows, where
the symlink lock that needs it never exists.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR 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

@juliusmarminge