-
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 | ✅ Working | Signs in end to end: the consent page's Authorize button completes, the extension exchanges the code, and the window dismisses itself. Also 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 of them specific to Claude: the bridged sender's origin taken from the page URL instead of Safari's sender.origin, the relay probing both transports rather than 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. 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). |
| 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 stopped at the account wall, so anything behind the login (identity generation, autofill, one-time passwords) is unverified rather than known broken. Five fixes to get that far, 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), and the inert onHistoryStateUpdated stub its page bridge waits on (0949657). |
|
| 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. Tampermonkey builds its own script sandboxes: it publishes content.js and page.js through chrome.userScripts, where content.js installs a pagejs setter on a sandbox global of its own making and page.js is the assignment meant to trip it from inside that sandbox. Injecting both into the isolated world loads the runtime and wires nothing. 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. It's the same sandbox-wiring problem described in the stable row, and this newer build hasn't changed it. |
|
| 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 | Re-tested on 1.71.0: the popup now comes up blank — a regression from the June build, where the counters rendered. Its boot RPCs go unanswered, so popup-fenix.js throws undefined.split on firewallFilters and punycode chokes on an undefined hostname. uBlock privilege-gates its popup port with sender.origin === getURL("").slice(0,-1) (vapi-background.js), and unlike LastPass (message-based, fixed) this rides a port whose read-only sender Safari won't let the shim rewrite — so it needs its own fix and is still open. Network blocking can't work on Safari either way: uBlock is a blocking-webRequest blocker, so use uBOL (pure declarativeNetRequest) for real blocking. |
|
| 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. |
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.