Skip to content

Limitations and FAQ

magicelk235 edited this page Aug 19, 2026 · 5 revisions

Limitations and FAQ

An honest catalog of what conversion cannot fix. Almost everything on this page is a Safari / WebKit / Apple platform constraint, not a viaduct bug. viaduct's job is to document these clearly, stub what it can so the extension still loads, and point you at the real remediation, so you don't file a bug for something no shim can change.

If something here bites you, please don't open a conversion bug. Read the remediation, and if you still think it's wrong, use the issue taxonomy at the bottom to pick the right template.


Contents


Limitations, what conversion can't fix

Each entry below follows the same shape: What breaks, Why (the platform reason), Real fix.

1. Blocking webRequest can't block in Safari

What breaks. Ad/content blockers built on the blocking form of webRequest, full uBlock Origin, AdGuard's blocking engine, and similar, do not block network requests in Safari. The extension still installs, and its cosmetic / element-hiding features (CSS injection that hides ad slots) still run. viaduct reports this class of extension with an error.

Why. WebKit decides whether to allow, redirect, or cancel each request before any extension JavaScript runs, and it ignores the blocking return value your listener produces. The decision happens below the JS layer, so there is nothing for a shim to intercept, no polyfill can change an outcome that is already decided by the time your code executes.

Real fix. Convert the extension's declarativeNetRequest build instead. Safari honors DNR rulesets, so a DNR-based blocker blocks for real once converted. For uBlock Origin specifically, that means uBlock Origin Lite (uBOL), which ships as a pure-DNR extension. Convert uBOL, not full uBO. (dbe3442 added the analyzer flag for this class; 644e97e documents the uBO → uBOL guidance.)

2. chrome.identity / hardcoded OAuth redirect can't complete login

What breaks. Extensions that authenticate with chrome.identity (launchWebAuthFlow / getAuthToken) or a hardcoded chrome-extension:// OAuth redirect URI cannot complete login after conversion. The shim stubs the call so it rejects cleanly instead of throwing, but the sign-in still fails.

Why. The OAuth client is registered on the identity provider's server against the original Chrome extension's identity and chrome-extension:// scheme. Safari can't reproduce that origin, every install gets its own per-install origin, so the redirect the provider expects never matches. This is registered state on someone else's server, not anything in your bundle.

Real fix. The provider has to register a Safari-compatible redirect or a hosted HTTPS callback flow. This cannot be fixed by conversion alone. See OAuth Bridge for the redirect patterns viaduct supports and how to wire a hosted callback.

2b. A site's frame-ancestors allowlist cannot name a Safari extension

What breaks. An extension page that embeds a first-party site in an iframe comes up blank, with Refused to load https://… because it does not appear in the frame-ancestors directive of the Content Security Policy in that page's console. Anything the extension does inside the frame is unreachable, including whatever UI lives there.

Why. The site decides who may embed it, and the list is written for Chrome. claude.ai serves this on the panel URL its extension embeds:

frame-ancestors 'self' chrome-extension://fcoeoabgfenejglbffodgkkbkcdhcgfn
                       chrome-extension://dngcpimnedloihjnnfngkgjoidhnaolf

A converted extension page is safari-web-extension://<per-install UUID>, which is not on that list and cannot be added to it: the UUID differs per install, and the header comes from the site's server. Nothing in the bundle influences the check.

Stripping the header is not available either. Safari's declarativeNetRequest cannot modify response headers, and a modifyHeaders rule takes down its whole rule store, which is why applyDnr removes those rules outright (see limitation 5). The native-host retry covers blocked API requests, not a frame navigation the browser refuses before any request leaves.

Real fix. The site adds a source the Safari origin can match, frame-ancestors safari-web-extension: being the practical one, since per-install UUIDs make an exact origin impossible. Until then, look for a non-embedded mode: the embed is often a newer opt-in path with the older native UI still shipping beside it. Claude for Chrome is exactly that shape, and its options page carries a "New side panel experience" toggle whose off position restores a side panel that works after conversion.

3. storage.sync doesn't sync across devices

What breaks. chrome.storage.sync reads and writes keep working, and data persists: but it does not sync across a user's devices.

Why. viaduct maps storage.sync onto storage.local because Safari has no iCloud-backed sync surface for web extensions. Local persistence is preserved; cross-device replication is simply not available on the platform.

Real fix. None at the extension layer for true sync. If cross-device state is essential, back it with your own hosted sync service. For most extensions, local persistence is enough and no action is needed.

4. Native messaging has no host manifest / host binary

What breaks. chrome.runtime.connectNative / sendNativeMessage have no Chrome-style host manifest and no separate host binary in Safari. The analyzer flags any use.

Why. Safari routes native messages to the containing macOS app, not to a standalone host process registered by a JSON manifest. There is no equivalent of Chrome's native-messaging host registration.

Real fix. viaduct ships a native-messaging broker (the container app runs a loopback broker that can launch a Chrome native-messaging host from its on-disk manifest and pipe the stdio framing; the sandboxed app extension relays to it). But the broker only carries the frames, you implement the actual response in the app's SafariWebExtensionHandler (its beginRequest method). The permission stays; the host model changes. See Build and Install for where the handler lives in the generated Xcode project. (Broker added in 05cc244; usage flagging in a9c2e80.)

5. declarativeNetRequest modifyHeaders is stripped

What breaks. DNR rules with a modifyHeaders action are removed, from both static rulesets and dynamic updateSessionRules / updateDynamicRules calls. Any use case that depends on rewriting request/response headers, a CORS bypass, injecting an auth header, stripping X-Frame-Options, is not converted. Other rules in the same ruleset are kept.

Why. A modifyHeaders action crashes Safari's WebKit DNR rule store when the rule loads. Rather than let one header rule take down the whole ruleset, viaduct strips just the modifyHeaders rules and keeps the rest.

Real fix. Move header rewriting to a native-messaging proxy (route the affected requests through the native host, which can set the headers Safari's DNR won't). See Manifest Transform for the DNR pipeline.

Two related DNR warnings (rules are dropped, not crashed):

  • regexFilter: Safari supports only a limited regex subset and silently drops any rule it can't compile. viaduct warns when a ruleset uses regexFilter so you know coverage may be incomplete.
  • Rule-count overflow: enabled static rules over the count Safari honors (~the documented static-rule guideline, ~30,000) are ignored by Safari; viaduct warns.

6. APIs with no Safari equivalent are stubbed

What breaks. Chrome APIs that Safari simply doesn't implement (e.g. chrome.declarativeContent, and others in the compat table) are stubbed so the extension loads and doesn't throw, but the underlying feature doesn't work. For example, declarativeContent rules never fire (the shim stubs the rule constructors so registration succeeds silently).

Why. A stub is the least-bad option: without it, the missing API throws on first access and can take the whole extension down at load. Stubbing keeps the extension alive; it can't conjure a capability WebKit doesn't have.

Real fix. The analyzer reports each stubbed API with a suggested remediation: read that output; it's per-API. See Runtime Shim for what each stub does and Analyzer for how to read the report.

7. Platform-internal issues that are NOT converter bugs

Some failures you'll see in Safari originate inside WebKit / the OS, downstream of anything viaduct touched. They surface during real-world debugging of converted extensions, and chasing them in the converter is wasted effort, the extension's bundle is correct; the platform is imposing its own rules. Document them, don't file conversion bugs:

  • WebAuthn / passkey RPID mismatches. The relying-party ID the site asserts doesn't match the origin Safari presents for the extension context, so a passkey ceremony fails. This is origin/RPID policy enforced by WebKit + the authenticator, not a manifest or shim defect.
  • WASM-SDK / dynamic chunk loaders. viaduct already handles the common cases, webpack's async chunks are pre-registered in background.html so chunk loading keeps working, and wasm-unsafe-eval is preserved (Safari supports WebAssembly compilation and, unlike 'unsafe-eval', 'wasm-unsafe-eval' does not enable eval()). SDKs that fetch or compile WASM in ways WebKit refuses are a WebKit limitation, not a conversion failure.
  • Webfont CSP refusals. Safari refusing to load a webfont because of a strict/mismatched font-src is a CSP decision by the browser, independent of conversion.

If you hit one of these, treat it as a platform report (or a site/SDK issue), not a viaduct conversion bug.


Requirements

Recapping what you need to run viaduct at all:

  • macOS with a full Xcode install: not just the Command Line Tools. The packaging and build steps call xcrun safari-web-extension-packager and xcodebuild, which ship with Xcode. (This is also why viaduct is macOS-only: there is no Xcode on Windows or Linux.)
  • Node.js 18 or newer.
  • No runtime dependencies. TypeScript is the only dev dependency.

Verify your toolchain any time:

viaduct --doctor

--doctor checks that Xcode (not just CLT), the packager, and xcodebuild are present and reports what's missing. See CLI Reference for its full output.


FAQ

The extension installed but disappears when I restart Safari.

Expected for ad-hoc (unsigned) builds. Without --team, Safari only loads the extension while "Allow Unsigned Extensions" (Develop menu) is on, and that toggle resets every time Safari restarts. With --install, viaduct sets the toggle and bounces Safari for you (pass --no-safari-restart to skip the bounce).

For persistence across restarts, team-sign it: --team (or --team auto, or plain --install, which auto-detects your Team ID from Xcode or your signing certificate). A team-signed build loads without the unsigned toggle and survives quitting Safari. See Build and Install.

My toolbar button does nothing in Safari.

Safari does not dispatch action.onClicked on the toolbar button. viaduct wires a workaround at convert time: if your background registers action.onClicked, it installs a synthetic popup that replays the click to your listeners on open (a brief popover), or an in-page hotkey path that avoids the popover entirely.

The catch: the bridge only fires listeners it can statically determine at convert time. If your click handler is registered dynamically or indirectly, viaduct can't see it to bridge it. Check that your action.onClicked registration is a statically-determinable message. See Safari Quirks.

The popup / sidebar toggles but never appears.

Usually one of two dynamic-URL problems:

  1. use_dynamic_url: true on web_accessible_resources. This is Chrome-only; in Safari the resource fails to serve and chrome.runtime.getURL() hands back an unservable URL, so the panel 404s silently. viaduct clears it (sets use_dynamic_url: false) so the resource loads at its static extension URL. If you're on an older conversion, re-convert.
  2. The general dynamic-URL 404 class: a page requested at a rotated/dynamic URL that Safari never registered. Point the UI at the static extension URL.

Can I use this on Windows or Linux?

No, macOS only. Conversion depends on Xcode (safari-web-extension-packager, xcodebuild), which only exists on macOS. There is no Windows or Linux path.

Do I need to publish to the App Store?

No, not for development. For local dev / temp-load, no signing account is needed. For a build that persists across Safari restarts, use team signing (--team). The App Store is only for distribution to other users, not required to run your own converted extension.

How do I edit an extension's keyboard shortcuts?

In Safari → Settings → Extensions. Safari has no chrome://extensions/shortcuts page. Note also that Safari honors at most 4 command shortcuts with a suggested key; if your extension declares more, viaduct keeps the first four's default chords and strips the rest's, those commands still exist and stay bindable by the user in Safari → Settings → Extensions, they just ship without a default shortcut. Shortcuts also require a Ctrl/Command/Alt modifier or Safari ignores them.

How do I convert a real ad blocker?

Use the DNR-based build, not the blocking-webRequest one. For uBlock Origin that means uBlock Origin Lite (uBOL). Full uBO's blocking can't work in Safari (see Limitation #1); uBOL blocks for real because Safari honors DNR rulesets.

What's the fastest way to iterate?

--temp-load. It stages the extension for Safari 18+'s "Add Temporary Extension" (Develop menu), no Xcode build, no signing, fastest edit-reload loop:

viaduct ./my-extension.zip --temp-load

Then in Safari: Settings → Developer → enable "Allow Unsigned Extensions", then Develop → "Add Temporary Extension". See Testing and Debugging.

Free vs. paid Apple account, what's the difference for persistence?

A free personal Apple team works, but its provisioning profile expires about every 7 days: just re-run the command to re-sign. A paid Apple Developer Program account lasts about a year. Auto-detection reads the team Xcode cached (IDEProvisioningTeamByIdentifier / IDEProvisioningTeams, in both the com.apple.dt.Xcode and com.apple.dt.xcodebuild domains), then any provisioning profile on disk, then the codesigning identity in your keychain; only when none of them has a team does the build drop to ad-hoc signing.

I signed in to Xcode but viaduct still says "No Apple team found".

Xcode caches the team id only after it has provisioned something, and it does not create a development certificate until the first signed build, so a freshly added account can leave every on-disk source empty. Open Xcode → Settings → Accounts, select the account, Manage Certificates → + → Apple Development; that writes both the certificate and the cached team, and the next run detects it. Or skip detection entirely and pass the id: --team <TEAMID>, from developer.apple.com → Account → Membership details.

Until a team is found the run still completes — it warns, falls back to ad-hoc, and exits 0. Only a team that reached xcodebuild and came out ad-hoc anyway is a failure.


Issue taxonomy, which template to file

viaduct's three issue templates encode a real failure taxonomy. Pick the one that matches your symptom: filing under the wrong one slows triage, and many "bugs" are the platform limits above.

Your situation Template What to include
The extension failed to convert: viaduct exited with an error and produced no working output. Conversion Failure (conversion-failure) The Chrome extension (.zip or Chrome Web Store URL), the full terminal error output (paste it all), and your OS & Xcode version (e.g. macOS 15.2, Xcode 16.2).
It converted fine but misbehaves in Safari: no conversion error, wrong runtime behavior. Converted but broken (bug) Extension name/version, expected vs actual behavior (with Safari console errors), exact steps to reproduce (convert command → load → …), and Safari & macOS version.
You want a capability viaduct doesn't have yet. Feature request (enhancement) The problem/motivation (what you can't do today), a proposed solution, and any alternatives / workarounds you tried.

Before filing anything, check this page: if your symptom is a documented limitation (blocking webRequest, chrome.identity OAuth, storage.sync not syncing, modifyHeaders stripped, a stubbed API, or a WebKit-internal issue), it is working as designed and a bug report won't change the platform. The right move there is the listed remediation, or a Feature request if you have a concrete idea for a bridge viaduct could add.


See also: OAuth Bridge · Build and Install · Safari Quirks · Manifest Transform · Runtime Shim · Analyzer · CLI Reference · Testing and Debugging

Clone this wiki locally