Safari is supported on macOS only and requires the full Xcode app. The
build â convert â
xcodebuild â open pipeline covers build and dev;
preview and start are not available, and there is no live reload yet.--browser=safari to turn the same extension you ship to Chrome and Firefox into a Safari App Extension. Extension.js bundles your code, runs Appleâs safari-web-extension-converter, compiles the generated app with xcodebuild, and walks you through enabling it.
Requirements
Safari is macOS-only and needs the full Xcode app, not just the Command Line Tools. The converter (safari-web-extension-converter) and xcodebuild ship inside Xcode.app.
extension build/dev --browser=safari fail fast, before bundling, with guidance instead of a late, confusing error.
What it produces
extension build --browser=safari creates, next to your project:
The pipeline runs end to end: bundle â convert â
xcodebuild, plus open
the app â guided enable in dev (or with build --open). A plain build
stops after packaging and prints the open command instead.
name (for example, React Sidebar Example â bundle id dev.extensionjs.React-Sidebar-Example). The project targets macOS by default.
App identity and packaging options
Bothdev and build accept identity overrides (Safari targets only):
The same options can live in
extension.config.js (CLI flags win):
Building the web-extension bundle on other platforms
extension build --browser=safari works on Linux and Windows too: it produces
the complete dist/safari web-extension bundle and skips the Xcode packaging
step with a warning. This lets CI build the payload anywhere and a Mac (or a
macOS runner) do the convert + xcodebuild part later. dev --browser=safari
still requires macOS with Xcode, because a Safari dev loop without packaging has
nothing to run.
Enabling the extension in Safari
Local builds are ad-hoc signed (no Apple Developer account required), so Safari needs you to opt in once. When the app opens (dev, or build --open), Extension.js prints these steps and confirms when macOS has registered the extension:
- Safari ⸠Settings ⸠Advanced ⸠check âShow features for web developersâ.
- Safari ⸠Develop ⸠Allow Unsigned Extensions (this resets every time Safari restarts).
- Safari ⸠Settings ⸠Extensions ⸠turn on your extension.
âAllow Unsigned Extensionsâ resets each time you launch Safari. Re-enable it
after restarting Safari during development. A signed build (Apple Developer
ID) avoids this step and is part of the distribution workflow.
Developing with dev
extension dev --browser=safari runs a watch loop:
- First compile: full package: convert, build, open the app, and print the enable steps.
- On every save: incremental
xcodebuildresync (typically a couple of seconds) that updates the appâs resources from the freshly rebuiltdist/safari.
When the Xcode project regenerates
The Xcode project is generated once and reused for resyncs. It is regenerated (the converter runs again) when yourmanifest.json changes semantically, when
the app name / bundle id / platform changes, or when you pass
--force-regenerate. Regeneration replaces the project: customizations made in
Xcode (entitlements, capabilities, added files or targets) are discarded.
Only these signing settings are preserved automatically: DEVELOPMENT_TEAM,
CODE_SIGN_STYLE, and PROVISIONING_PROFILE_SPECIFIER. Extension.js warns
before every regeneration of an existing project; if you customized the project
in Xcode, back it up first. Delete dist/safari-xcode for a clean slate.
Debugging in Safari
Safari doesnât support the--logs centralized logger (there is no automation
channel), but Web Inspector covers every extension context:
- Background/service worker: Safari ⸠Develop ⸠Web Extension Background Content ⸠your extension.
- Popup/options/sidebar pages: open the surface, then right-click ⸠Inspect Element (or Develop ⸠your Mac ⸠the page).
- Content scripts: inspect the host page, where extension script contexts appear in the Sources tab under Extension Scripts.
xcrun/xcodebuild
output. Set EXTENSION_AUTHOR_MODE=true to stream the full tool output live.
The converterâs compatibility warnings (manifest keys Safari doesnât support)
are surfaced as warnings during packaging.
Engine target
safari has an engine alias, webkit-based, that parallels chromium-based and gecko-based:
Command support
preview and start exist to launch your extension in a running browser. Safari requires the manual, security-gated enable step above, so those commands point you to build instead.
Limitations
- Packaging is macOS-only. The Xcode step needs macOS with the full Xcode app. (
buildon other platforms still producesdist/safari;devrequires macOS.) - No live reload. Rebuilds are fast, but you refresh in Safari to apply them.
- Manual one-time enable. Allowing unsigned extensions and toggling the extension on are Safari security controls and cannot be automated.
- Local builds are ad-hoc signed. Distribution signing, notarization, and App Store submission are a separate step beyond this workflow (see below).
- macOS target only. iOS app generation is not produced by this workflow today.
After dev: shipping to the App Store
The Safari workflow above ends with an ad-hocâsigned local app. Distributing it (to the Mac App Store, or as a notarized direct download) is a separate pipeline that Extension.js plans to offer through the extension.dev platform. Until that lands, follow Appleâs own guides:- Distributing your Safari web extension (Apple Developer Program, signing, App Store Connect).
- Notarizing macOS software for nonâApp Store distribution.
- Set your own
--bundle-id(reverse-DNS, a domain you own) from the first build. Bundle id is the extensionâs identity on Apple platforms. - Set your
DEVELOPMENT_TEAMin Xcode once: it survives project regeneration automatically, along withCODE_SIGN_STYLEandPROVISIONING_PROFILE_SPECIFIER.
Best practices
- Build other targets normally: Safari is additive, so keep iterating in
chromium/firefoxand run--browser=safariwhen you want to validate Safari. - Use browser-specific fields for true behavioral differences. Safari resolves the chromium-family prefixes (
chromium:,chrome:,edge:), andsafari:/webkit:prefixed keys win over them on Safari targets, for both--browser=safariand--browser=webkit-based. - Keep the generated project unless you need a clean slate, because regenerating discards Xcode-side customizations beyond the preserved signing settings.
Next steps
- See all supported browsers.
- Use browser-specific manifest fields.
- Review multi-platform builds.

