-
Notifications
You must be signed in to change notification settings - Fork 3
Tested Extensions
Which extensions have actually been converted with viaduct and driven in Safari, and what happened. Nothing here comes from a clean --analyze run or a successful build: a row gets a status only after someone installed the extension and used it.
None of this is a promise. A new version of the extension, or of Safari, can invalidate any row. And when something fails because Safari can't do it at all, the row points at Limitations and FAQ instead of re-explaining it.
- How to read a status
- Verified on the current build
- Verified in the June corpus round
- Converted but not driven yet
- Convert something else instead
- Adding a result
| Status | Meaning |
|---|---|
| ✅ Working | Installed, enabled, and the extension's main job works. Minor cosmetics may still be off. |
| Loads and runs, but a real feature is missing. The row says which, and whether it's a viaduct gap or a platform limit. | |
| 🚧 Untested | Converts and builds. Nobody has driven it in Safari yet, so it has no result. |
| ❌ Blocked | Converts, but the thing you install it for cannot work on Safari. See Limitations and FAQ. |
"Version" is the version of the Chrome extension that was converted. Extensions ship fast, so a newer build may behave differently.
| Extension | Version | MV | Status | Exercised |
|---|---|---|---|---|
| Dark Reader | 4.9.128 | 3 | ✅ Working | Pages restyle dark, popup and its controls render, per-site toggles stick. |
| Claude in Chrome | 1.0.85 | 3 | Signs in end to end: the consent page's Authorize button completes, the extension exchanges the code, and the window dismisses itself. The popup and side panel toggle, including the Cmd+E open/close cycle that used to spawn a stray tab every other press (4d1109a). The login took four fixes, none specific to Claude: the bridged sender's origin taken from the page URL rather than Safari's sender.origin, the relay probing both transports instead of assuming one sendMessage lands, the page bridge re-injected as a web-accessible <script> when Safari ignores its world:"MAIN" entry, and the bridged sender carrying a tab so the handler can navigate the consent window away. What does not work is the new embedded side panel. With "New side panel experience" on, the panel loads claude.ai/cic/new in an iframe and claude.ai's frame-ancestors names only its two chrome-extension:// origins, so Safari refuses the frame and the panel sits empty. That is the site's header, not a conversion gap, and no rule can reach it (see Limitations and FAQ 2b). Turning the toggle off, or "Switch back to classic" in the panel menu, restores a working panel. Earlier rows read 1.0.80 with the login unexercised, which is why the bridge looked healthy for so long. See OAuth Bridge. |
|
| TWP - Translate Web Pages | 10.1.1 | 2 | ✅ Working | Toolbar button opens the real popup, page translation runs end to end. Took three fixes: the background crash and double content-script injection (7247e8b), the setPopup hijack that replaced its popup with a gray stub (6f63db2), and keeping content scripts on native messaging (4010230). |
| TWP - Translate Web Pages | 10.2.5 | 3 | ✅ Working | The MV3 rewrite of the same extension, and a completely different failure from 10.1.1's: its webRequest permission routed the converted background into the shim's url-sanitizer, whose global-root republish silently detached the page from all message dispatch, so every translateHTML resolved undefined and no page translated (Safari Quirks E15, 895e7a3). Its context-menu entries also never existed, menus.create threw on file://*/* and page_action (Safari Quirks F4, cf96d63). Verified live after both fixes: popup translate turns the page English; earlier 10.2.5 conversions (July 2026 onward) were broken the whole time. |
| Cloaked - Privacy & Password Manager | 3.6.2 | 3 | Popup renders, and clicking Log in opens a real Cloaked sign-in window with the extension listening on the other side. Testing here stopped at the account wall, so anything behind the login (identity generation, autofill, one-time passwords) is unverified rather than known broken. A user with an account then reported the next step failing: the dashboard shows “Extension Authentication Successful” and closes itself, and the extension is still on its Log In screen. Cause found and fixed, and it is not about tokens: Cloaked installs its page↔extension bridge from webNavigation.onHistoryStateUpdated, and its dashboard pushes the extension-auth status route programmatically once the token exchange returns — seconds after the last click. Measured on Safari 26, that push re-injects no content script and produced no event, so the bridge was never installed and the dashboard's broadcast had nobody to hand the session to. The content script now watches location.href continuously wherever the extension actually listens for the event, and the whole round trip (report → event → tabs.sendMessage back to that tab) is measured working (Safari Quirks C5). Six fixes to get here, every one of them cross-cutting: content scripts kept on native messaging (4010230), the relay Proxy that made Safari stop delivering content-script messages to extension pages (8acef29), a background page that reported FOREGROUND to its own dispatcher (cd58700), windows.create handing back a Window with no tabs (7fd15e8), the inert onHistoryStateUpdated stub its page bridge waits on (0949657), and that event missing the one navigation the login depends on. The login itself is pending a retest by someone with an account. |
|
| Honey: Automated Coupons & Rewards | 19.4.0 | 3 | ✅ Working | Popup renders, and a session signed in on joinhoney.com is recognized after the popup is reopened. Took four fixes, all of them shim-wide rather than anything to do with coupons: the storage relay's own sender.url keeping its query (11a00b4), ?tabId being injected into an action popup (064812b), getURL("") and getURL("/") being lowercased while location.href was not (bf014e0), and tabs.onActivated never firing in the background (7b1b69a). Coupon application at checkout is unverified. |
| Tampermonkey | 5.5.0 | 3 | Dashboard, settings and the editor all work, a userscript saves and persists, the version renders, and its registered runtime is now delivered to and evaluated on every page. Userscripts still have no effect, but the sandbox-setter explanation this row used to carry was a misdiagnosis: that code path is Tampermonkey's Firefox branch, gated on chrome.userScripts.onBeforeScript, which Safari (and Chrome) never has, and the Chrome-path shared-global handshake between content.js and page.js is already satisfied by the injector — test/userscripts-injection.test.js proves it with the real registration payloads. The remaining suspects are the injector's late (post-storage.local-read) start defeating document_start, the page CSP applying where Chrome's USER_SCRIPT world is exempt, and the raw sandbox's detached-iframe realm, which WebKit may discard; Settings → Sandbox mode DOM sidesteps the last one and is the first thing to try with a --debug build. See Safari Quirks C3. Five viaduct bugs were fixed getting this far (3403ff7, 3dd05f2, 479d518, 937da18, 62ec130), all of which affected any extension shaped the same way. |
|
| Google Translate | 2.0.17 | 3 | ✅ Working | Popup renders and typing a phrase returns its translation. The popup also points at Chrome's built-in page translation, which is a browser feature rather than anything the extension drives, so there's nothing there for viaduct to convert. The analyzer flags a Chrome UA-version sniff in the popup and options that the shim doesn't spoof outside content scripts, but it didn't stop the popup working here. |
| Requestly | 26.6.10 | 3 | Converts and installs, and the popup renders with the "Requestly running" toggle live. The core rule engine can't be exercised, but not because of the conversion: Requestly's own dashboard detects Safari and refuses to load HTTP Rules and Sessions ("Currently, only the API Client feature works on Safari"), pushing you to their desktop app or Chrome. That's a vendor gate on their side, not a viaduct gap or a Safari platform limit the shim could close. The background console shows a burst of failed ws://127.0.0.1:5976x connections; that's serviceWorker.js scanning local ports for the Requestly desktop app, and it fails the same way in Chrome when that app isn't running, so it's not a conversion fault. |
|
| Session Buddy | 4.1.2 | 3 | ✅ Working | The toolbar popup is intentionally empty and opens the manager in a full page (same as Chrome). The manager reads every open tab across all windows, and saving the current window writes a collection that shows up under COLLECTIONS, so tabs.query and the storage relay both work. Restore and survival across a full Safari restart weren't re-checked in this pass. |
| Tampermonkey BETA | 5.5.6237 | 3 | Same result as the stable build one version back: the dashboard and editor open, a userscript saves and stays enabled, but it never runs. A trivial @match https://example.com/* script set to alert, redden the page, and rewrite the title did none of the three on load. Same open question as the stable row — see there for the corrected diagnosis and the sandbox-mode test to run next. |
|
| Grammarly | 14.1304.0 | 3 | ✅ Working | Re-verified on 1.9.x: the popup renders and the account loads, settings open, and the in-field checker flags bad grammar on a live page. Still standing on the June fix chain, whose unblocker was a conversion-time rewrite stripping the runtime.id + prefix out of the popup's port-routing regex, because Safari's runtime.id is the App-Extension bundle id, not the UUID in extension URLs (72c50ad). |
| LastPass | 4.154.0 | 3 | ✅ Working | Popup renders and reaches the login screen after a shim fix. 4.154.0 used to hang on the loading splash: the popup boots via stateSync = chrome.runtime.sendMessage({initialize}) and the background answers only past a sender gate new URL(sender.url).origin === sender.origin. Safari serves getURL("") with an UPPERCASE UUID host while every origin is lowercase (live-confirmed: getURL("")=…A000EA25…, location.origin=…a000ea25…), and the shim was rewriting the message sender's origin up to that uppercase (to satisfy uBlock's sender.origin === getURL("").slice(0,-1)), so LastPass's lowercase new URL(...).origin never matched → Cannot destructure property 'state' from null. Fixed by lowercasing the host for the empty-arg getURL("") only (real case kept for / and resource paths), regression-tested in test/geturl-host-case.test.js. Behind the login wall (vault, autofill) is unverified. |
| uBlock Origin | 1.71.0 | 3 | The popup renders again, and strict blocking works — both verified live after the port-sender fix. The popup had come up blank since the June build: uBlock privilege-gates its popup port at connect time, privileged = origin !== undefined ? origin === PRIVILEGED_ORIGIN : url.startsWith(PRIVILEGED_ORIGIN) (vapi-background.js), with PRIVILEGED_ORIGIN = getURL('').slice(0,-1), and neither branch could pass on Safari: the port's sender is an exotic read-only getter the shim's old port.sender = fixed assignment silently bounced off, and the url fallback compares Safari's UPPERCASE sender.url against the lowercase getURL("") base. The onConnect wrapper now installs the corrected sender in place where the slot allows it, and otherwise hands listeners a memoized delegating Port whose sender carries the canonical origin (Safari Quirks B4, test/port-sender-origin.test.js). Strict blocking survives because it never depended on the blocking return value: a w3schools ad's cookie-sync navigation to sync.richaudience.com was replaced with uBlock's interstitial, citing ||richaudience.com^ from Peter Lowe's list — which also proves list loading, the filter engine, and webRequest event delivery all work (Safari Quirks D4). A console error about assets/thirdparties/pgl.yoyo.org/as/serverlist.txt being missing is uBlock's own: its assets.json lists that path as a fallback candidate it has never shipped (the bundled file is extensionless serverlist, the next candidate, which loads); Chrome fails the same fetch quietly. One strict-block misfire was found and fixed the same day: Safari's webRequest labels every iframe document load main_frame (Safari Quirks D4a), so visiting w3schools.com got the whole tab replaced by the interstitial over an ad iframe's cookie-sync — the shim now normalizes the mislabel to sub_frame and the interstitial should only appear for real top-level navigations (fix in test/webrequest-frame-type.test.js, re-test pending). Subresource blocking still can't work — {cancel:true} for scripts/images/frames is ignored by WebKit, so in-page ad requests go out and the counters stay near 0; hiding is cosmetic. For real network blocking use uBOL (pure declarativeNetRequest). Cosmetic filtering is unverified. |
|
| Kondo | 1.12.1 | 3 | ✅ Working | Reported dead by a user ("nothing is working", issue #1). Every path the conversion owns now runs: the web app at app.trykondo.com signs in, loads the inbox shell and reaches Kondo's own plan-selection screen (so the background answered /voyager/api/me with the LinkedIn session it read out of Safari's cookie jar); visiting linkedin.com/messaging fires the second content script and auto-opens the Kondo inbox; and the toolbar button's context menu carries Kondo's own contexts:["action"] items with "Open Kondo automatically" correctly ticked, so contextMenus and the storage.sync read behind it round-trip. Anything behind Kondo's paid plan (inbox sync, badge counts, notifications, reminders, the multi-profile cookie swap) is unverified rather than known broken — the account used for testing has no subscription. Two fixes, both general: Kondo's only channel is a MessagePort handed to the background through navigator.serviceWorker from a hidden ext.html iframe, which never resolves on a converted extension (Safari Quirks C7); and once the background did run, cookies.get returned a stale .linkedin.com JSESSIONID instead of the real .www.linkedin.com one, so every LinkedIn call carried a Csrf-Token contradicting its own Cookie header and came back 403 "CSRF check failed" (Safari Quirks E8). The toolbar popup still looks broken and isn't: Kondo's popup only does window.open(app); window.close(), the tab opens, and Safari won't let script close the popover it drew (A5), so an empty grey panel lingers. |
| Replace AI Translator API | 1.0.21 | 3 | ✅ Working | Reported by a user: converted with no error or warning, then the popup would not scroll and clicking a provider in API settings did nothing. Two unrelated causes, both fixed and both measured in the real popover. Its worker imports its libraries by root-absolute path (importScripts("/src/lib/actions.js", …)), which the hoist resolved against the worker's own directory instead of the extension root, so background.html got none of them and every message handler threw on the first missing global — the popup's GET_STATE came back {ok:false, error:"exception"} in 11 ms and left its settings null (Safari Quirks G1a). And Safari's popover cannot scroll its main frame at all: the viewport measured 360x600 against a 989-tall document with scrollTo a no-op, so a third of the UI was unreachable (Safari Quirks F5). After both: settings load, Page Down and Page Up move through the whole popup, and picking Anthropic selects the card and persists (provider=anthropic read back from the background). Its Chrome-version UA sniffs and its chrome://extensions/shortcuts link still don't apply on Safari; the analyzer warns about both. |
Between them these cover most of the pipeline: manifest transform, shim injection, message routing, action wiring, and the MV3 service-worker to background-page conversion.
These were live-tested against the complexity corpus (see test extensions/rating) around 2026-06-23. They have not been re-checked since, and about four minor releases of shim and manifest work have landed on top, so read them as history rather than current state. Re-running any of them is a genuinely useful contribution.
| Extension | Status | Result at the time |
|---|---|---|
| Bitwarden | UI loads and works. The WASM SDK and passkeys are platform limits, not conversion bugs. | |
| Loom | Shim intact, popup blank while logged out (it opens web signup by design). tabCapture / desktopCapture are dropped, so recording is a platform limit. Logged-in flow unverified. |
|
| Honey | Background runs, network works, and a real setBadgeText init-abort got fixed out of it. Popup still comes up empty: stores:action never gets a reply on Safari. Resolved on 1.9.x, see the row above; the diagnosis in this line was wrong, stores:action was being rejected rather than dropped. |
The cross-cutting bugs this round turned up (frozen API roots aborting the shim, the immutable chrome.scripting slot, extension-UUID case mismatch, getURL("") on a frozen runtime, connect to a suspended background) are documented in Safari Quirks with the fixes that came out of them.
These build and install. Nobody has sat down with them in Safari, so they have no result yet.
| Extension | Version | MV |
|---|---|---|
| ChatGPT | 1.2.27221 | 3 |
| Jump Cutter | 1.32.2 | 3 |
| Salesforce Inspector Reloaded | 1.271 | 3 |
| Inject Control | 1.0 | 2 |
Tampermonkey ships a stable and a BETA build one version apart from the same code; both are in the table above now, with the same result. The stable one is worth reading as a case study: getting it from "installs and registers fine" to "saves a script" took five separate fixes, and only the first two were visible before install. It ships chrome://favicon/ in permissions, which made Safari reject the whole manifest (31d13b9), and it declares webRequestBlocking over <all_urls> while building its DNR rules at runtime, which made the content-blocker check mistake a userscript manager for an ad blocker and abort the conversion (3403ff7). The other three only showed up under live use.
| Instead of | Convert | Why |
|---|---|---|
| uBlock Origin (full) | uBlock Origin Lite (uBOL) | Blocking webRequest doesn't block on Safari. WebKit decides each request before extension JS runs and ignores your listener's return value, so no shim can change the outcome. uBOL is pure declarativeNetRequest, which Safari honors. |
Any extension whose core feature is blocking webRequest, chrome.identity OAuth, cross-device storage.sync, or tabCapture lands in the same bucket. Limitations and FAQ has the full list with the real remediation for each.
Convert, install, and actually use the extension:
viaduct "test extensions/Some Extension.zip" --install --force --cleanThen open the background page in Safari's Web Inspector (Develop, Web Extension Background Pages) and drive the feature you'd normally use it for. The background console is reliable; the popover console is not, which is why Testing and Debugging recommends writing diagnostics to chrome.storage.local and reading them from the background.
Record a result once you can say what you did and what happened, along the lines of "popup renders and account loads". A build that merely succeeded doesn't tell anyone much. For a failure, open an issue with the converted-but-broken template, and include the extension version you used, since the next release may behave differently.
Viaduct CLI · @magicelk235/viaduct · PolyForm Shield 1.0.0 · Verified against src/ and grounded in git history.