feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox - #7260
feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc and Firefox#7260juliusmarminge wants to merge 16 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: unavailable · PR result: Scenario and decoded snapshot size10 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.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
bc1215b to
973b17e
Compare
There was a problem hiding this comment.
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
ApprovabilityVerdict: Needs human review 1 blocking correctness issue found. New feature adding browser import support for 7 browsers with substantial new logic, cross-platform path handling, and a new Firefox cookie extraction engine. New capabilities of this scope warrant human review. An unresolved High severity finding regarding Firefox cookie domain handling also requires attention. No code changes detected at You can customize Macroscope's approvability policy. Learn more. |
973b17e to
1b132e4
Compare
| url: `${secure ? "https" : "http"}://${host}${row.path}`, | ||
| name: row.name, | ||
| value: row.value, | ||
| domain: row.host, |
There was a problem hiding this comment.
🟠 High BrowserImport/FirefoxCookies.ts:75
Host-only Firefox cookies are imported with domain: row.host, so Electron normalizes example.com to a domain cookie and exposes it to every subdomain. Omit domain when row.host has no leading dot (and update the cookie type if needed) to preserve the original scope.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/desktop/src/preview/BrowserImport/FirefoxCookies.ts around line 75:
Host-only Firefox cookies are imported with `domain: row.host`, so Electron normalizes `example.com` to a domain cookie and exposes it to every subdomain. Omit `domain` when `row.host` has no leading dot (and update the cookie type if needed) to preserve the original scope.
3343565 to
56f1705
Compare
There was a problem hiding this comment.
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
56f1705 to
4d48bd9
Compare
4d48bd9 to
0d85caa
Compare
7a8cf25 to
27037c9
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix is ON, but a cloud agent failed to start.
Reviewed by Cursor Bugbot for commit 27037c9. Configure here.
27037c9 to
9328514
Compare
9328514 to
de17611
Compare
de17611 to
333c68e
Compare
There was a problem hiding this comment.
One retained finding in the Firefox error model; the rest of the Effect service conventions look satisfied (namespace subpath imports, tagged errors at the failure boundary, Context.Service/make/layer order preserved, no new runtimes or injected service instances).
Posted via Macroscope — Effect Service Conventions
333c68e to
c2e27ca
Compare
The desktop app needs to read browser cookie databases and should reach for the same Effect SQL client the server already uses rather than opening `node:sqlite` by hand. Nothing about the client is server-specific, so it moves to `packages/shared` and both apps import it from there. Pure move plus import updates; the client itself is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds per-profile cookie import, with Helium on macOS as the first source. Cookies carry the logged-in sessions, which is what makes an imported profile useful; saved passwords are out of scope because Electron exposes no password store to put them in. The key is read through the in-process Keychain API rather than by shelling out to `/usr/bin/security`, because macOS attributes both the consent prompt and the resulting ACL grant to the binary that asks. Via the CLI the prompt said "security" and "Always Allow" granted trust to a tool every process on the machine can invoke; in-process it names this app and the grant belongs to it. There is deliberately no fallback when consent is denied — the techniques that work around it exist to defeat exactly this. The read is untimed: macOS answers it with a modal, and a timeout racing the user means the prompt can be approved after nothing is left listening, which reads as "approving did nothing". Sources pin their own coordinates rather than deriving them, because Chromium forks disagree: Helium uses the keychain service "Helium Storage Key" / account "Helium" where Chrome and its closer relatives use "<Name> Safe Storage" / "<Name>". `expires_utc` counts microseconds since 1601 and exceeds JavaScript's safe integer range, which `node:sqlite` refuses to narrow, so the division happens in SQL and only seconds cross the boundary. The cookie database is snapshotted before reading, since Chromium keeps it open with WAL. Unavailable sources are reported with a reason rather than as one generic failure — a missing keychain item is not something approving a prompt can fix. `unsupportedPlatform` is the one that is not a permission at all: it covers cases like Chrome on Windows, whose App-Bound Encryption is designed to stop this and which we will not work around. The platform binary is staged beside the loader during packaging, mirroring the Clerk passkey handling: pnpm nests the arch package, electron-builder only retains top-level dependencies, and the generated loader checks for a sibling `.node` first. Verified end to end against a real Helium profile: 5,002 cookies imported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The list was a loose stack of rows with two competing controls and a separate "Default browser profile" setting duplicating what the list already showed. Profiles now render as a table. Which one is default is a badge on its row and is changed from that row's menu, so the standalone setting is gone. Each row also gains "Clear cookies and cache", scoped to that profile's partition. Creating and importing collapse into one "Add profile" menu, because from the user's side they are one decision: "I want a profile with my Helium logins in it". Each source offers its targets directly — New profile, or any existing one — so importing into a fresh profile no longer means creating it first and then hunting for a second control. Previously import offered no choice of target at all. Incognito is not listed. It keeps nothing between launches, so it has no data to clear, no name to edit, and no state to manage; it belongs in the menu that opens a tab. It is also excluded as an import target, since importing into it would be discarded on quit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sers `importCookies` forwarded the IPC-supplied `sourceProfileDirectory` straight into the cookie database path, so `..` segments walked out of the browser's user-data directory and imported any cookie database reachable on disk. It is now only honoured when the source itself reported it. The running-browser check used `access` on Chromium's `SingletonLock`, which is a symlink to a `<host>-<pid>` target that never exists. Following it reported every running browser as closed, so an import could read a live, mid-write database — Helium open on this machine was detected as closed. It now stats the link itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The import module reached for `node:fs/promises`, `node:os`, `node:path` and `node:sqlite` directly and threaded results through hand-rolled promises, which meant a blanket `nodeBuiltinImport:off` on two files and failures that were plain thrown `Error`s. It now uses FileSystem, Path and the shared Effect SQL client, and the cookie snapshot is a scoped resource rather than a try/finally with a cleanup callback. `node:crypto` stays — it implements the OSCrypt primitives Chromium uses and has no Effect equivalent — with the suppression narrowed to it and a reason attached. Failure reasons are a typed union in contracts rather than free strings, so the renderer maps a reason to copy instead of substring-matching an error message. Porting `isSourceRunning` to `FileSystem.stat` reintroduced the dangling SingletonLock bug, because `stat` and `exists` both follow symlinks; `readLink` is the probe that answers for the entry itself. The regression test caught it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removing a profile deleted it and wiped its cookies and cache from a single icon-menu click, while every comparable destructive action in Settings confirms first. It now routes through the same AlertDialog. The Default badge resolved against the unfiltered profile list while the table renders only non-incognito rows, so a stored default of "incognito" left the section with no default marked at all. It now resolves against the rows that render. Reopening the import menu kept the previous source list on screen while the refresh was in flight, leaving a source that had since become unavailable selectable; the list is cleared first so the menu shows its loading state. A cookie sidecar that exists but cannot be copied is no longer ignored alongside the missing-file case. SQLite would open the snapshot without the write-ahead log and return a cookie set silently missing its newest transactions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ChromiumCookieReadError` carried only a reason and a cause, so every `readFailed` and keychain refusal logged identically. A user with several Chromium browsers installed had no way to tell which one refused. The database path is now a structural attribute and the message derives from it, matching how `BrowserSession`'s errors carry their partition. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Renaming the component left three consecutive doc blocks above it, two of them describing controls that no longer exist separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Detection keyed off the browser's user-data directory, which is not evidence the browser exists. Installers for native messaging hosts create an empty one for every Chromium fork they know about, so a machine with only Chrome and Helium listed Edge, Brave, Vivaldi, Opera and Arc as importable sources — each holding nothing but a `NativeMessagingHosts` folder. It now keys off the cookie database, which is the thing an import actually needs. Existence is checked without opening the file, which matters for Safari: TCC permits `stat` on the jar inside its container but refuses a read, so Safari is still found and the user gets the Full Disk Access prompt instead of Safari vanishing from the list. A source that is not on the machine is now left out of the menu rather than shown as a dead row. Every other unavailable reason stays visible, because each names something the user can do — quit the browser, grant access. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`importInto` created the target profile before `runImport` checked for an environment and a bridge, so choosing an import target before the environment resolved left a new empty profile named after the source browser and produced no toast at all. The check now happens first and says what went wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`clearProfileData` returned silently when no environment was connected, while the menu item stayed enabled — a dead control with no explanation. It now reports the same way `importInto` does for the same precondition. Also switches `browserImport` to the subpath namespace import the rest of `packages/contracts` uses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…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>
…ontainers apart 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. Opera does not follow the local-AppData `User Data` convention its Chromium relatives use — it lives under roaming `%APPDATA%\Opera Software\Opera Stable` — so it was never detected on Windows. 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>
c2e27ca to
7487b3c
Compare
There was a problem hiding this comment.
One finding: the mapped error union is discriminated by hand instead of with Effect.catchTags.
Posted via Macroscope — Effect Service Conventions
| new BrowserImportFailedError({ | ||
| sourceId: definition.id, | ||
| // Firefox has one failure mode — its plaintext database would not | ||
| // open — so its error carries no reason of its own and the | ||
| // user-facing one is supplied here. | ||
| reason: cause._tag === "FirefoxCookieReadError" ? "readFailed" : cause.reason, |
There was a problem hiding this comment.
Both failures are statically known tagged errors, so the branch belongs in Effect.catchTags rather than a hand-rolled cause._tag ternary inside mapError. That also keeps each handler's reason next to the tag it applies to.
The comment above read ("Both branches fail with a tagged error carrying a reason") is now stale too — FirefoxCookieReadError deliberately has no reason.
const cookies = yield* read.pipe(
Effect.scoped,
Effect.provide(platformServices),
- Effect.mapError(
- (cause) =>
- new BrowserImportFailedError({
- sourceId: definition.id,
- reason: cause._tag === "FirefoxCookieReadError" ? "readFailed" : cause.reason,
- cause,
- }),
- ),
+ Effect.catchTags({
+ // Firefox has one failure mode — its plaintext database would not
+ // open — so the user-facing reason is supplied here.
+ FirefoxCookieReadError: (cause) =>
+ new BrowserImportFailedError({ sourceId: definition.id, reason: "readFailed", cause }),
+ ChromiumCookieReadError: (cause) =>
+ new BrowserImportFailedError({ sourceId: definition.id, reason: cause.reason, cause }),
+ }),
);Posted via Macroscope — Effect Service Conventions

Generalises the importer from one hardcoded browser to a source registry with two engines, taking it from 1 source to 8.
¹ Chromium off macOS reports
unsupportedPlatformuntil its key stores land — that's the next PR.Notes for review
Chromium forks are table entries. They share the existing extractor and differ only in paths and keychain coordinates. Those are pinned per fork rather than derived, because the forks disagree — Helium uses
Helium Storage Key/Heliumwhere the others use<Name> Safe Storage/<Name>.Firefox is a second engine with no crypto at all. It stores cookies in plaintext
cookies.sqlite, so there's no key and no consent prompt. That's Mozilla's design choice, not a control being circumvented, and it's why Firefox works identically on all three platforms while Chromium needs a per-platform credential store.Paths resolve from an injected context, not
process, so Windows and Linux layouts can be checked without running on them.The snapshot-before-read step moved to a shared module. Both engines keep the database open with WAL, and neither may have the browser's own file opened for writing.
Testing
Verified against the machine this was written on — all seven Chromium sources detected, Chrome reporting its real profile name:
Firefox isn't installed here, so it's covered by tests against a real
moz_cookiesfixture rather than a claim — including one asserting the source file's mtime and size are unchanged after a read.profiles.iniparsing has its own tests because of a genuine trap:Install*sections name a default profile without describing one, so counting them invents a directory that doesn't exist.Note
Add Chrome, Edge, Brave, Vivaldi, Opera, Arc, and Firefox as browser import sources
BROWSER_IMPORT_SOURCESin Sources.ts from Helium-only to all major browsers, with correct per-platform user-data paths for macOS, Windows, and Linux.readFirefoxCookiesreader in FirefoxCookies.ts that readscookies.sqlitedirectly (no keychain), filtering to default-container cookies only.BrowserImportPathContextto unify platform, home, and appData resolution, replacing separate platform and path arguments acrossisSourceInstalled,isSourceRunning,listSourceProfiles, andcookieDatabasePath.ImportedCookietype into CookieDatabase.ts so both Chromium and Firefox readers share them.unsupportedPlatformerror on other platforms); Firefox imports are cross-platform.Macroscope summarized 7487b3c.
Note
Medium Risk
Touches local filesystem reads, keychain-backed decryption, and session cookie writes with path traversal guards; Chromium is deliberately gated off macOS, but wrong paths or lock detection could still read live DBs or mis-report availability.
Overview
Expands browser cookie import from a single Helium entry to eight sources (Chrome, Edge, Brave, Vivaldi, Opera, Arc, Helium, Firefox) via a registry with
chromiumandfirefoxengines, pinned per-fork paths/keychain coordinates, and achromiumSourcehelper for macOS/Windows/Linux layouts (including Opera under roaming AppData).Replaces
SourcePaths/sourcePathswithBrowserImportPathContext/sourcePathContext(platform, home,%APPDATA%/%LOCALAPPDATA%) so listing, install detection, running checks, and DB paths are testable off-host. Chromium imports stay macOS-only until non-mac key stores exist; Firefox is cross-platform with no keychain.Adds Firefox:
profiles.iniparsing, per-profile lock detection (not rootSingletonLock),cookies.sqliteviareadFirefoxCookies(default-container cookies only viaoriginAttributes = ''). Moves WAL snapshot +ImportedCookieintoCookieDatabase.ts; Chromium uses the shared type and snapshot.BrowserImport.importCookiesbranches on engine, mapsFirefoxCookieReadErrortoreadFailed, and treats missing cookie DB paths asunsupportedPlatform. Contracts extendBROWSER_IMPORT_SOURCE_IDSto all eight browsers.Reviewed by Cursor Bugbot for commit 7487b3c. Bugbot is set up for automated code reviews on this repo. Configure here.