Skip to content

Safari Extension (Developer Mode)

Javad Rajabzadeh edited this page Sep 23, 2026 · 1 revision

Safari does not accept an extension the way Chrome or Firefox do. There is no unpacked directory to load and no file to drag in: a Safari web extension ships inside a macOS app, and the app registers the extension with the system when you run it. Until the extension is signed with an Apple Developer ID and notarized, Safari treats it as unsigned and only loads it while developer mode is armed.

This page is the whole route, start to finish, for a build you made yourself or downloaded from a release.

What works on Safari, and what cannot

Feature Chrome / Firefox Safari
Right-click → Download with Hydra yes yes
Selection pill over highlighted links yes yes
Download all links yes yes
Popup, capture toggle, status badge yes yes
Media / HLS / DASH sniffing yes only where Safari exposes webRequest
Automatic download capture yes not possible

Safari has no downloads API — Apple exposes no download events to extensions at all, so there is nothing to intercept. This is a platform limit that every download manager on macOS shares; it is not a gap in Hydra. Use the right-click item or the selection pill instead.

Requirements

  • macOS 11 or later (the wrapper app's deployment target), Safari 16.4 or later.
  • Full Xcode, if you are building it yourself. Command Line Tools alone cannot build an app extension:
    sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
  • Hydra itself installed and running. On Safari the extension can only reach an app that is already up — see Known limits.

1. Get the app

Either take hydra-safari-extension-<version>.zip from a release and unzip it, or build it from a checkout:

scripts/build-safari-extension.sh

The script syncs the web resources from extensions/chrome into extensions/safari/Resources, generates the wrapper Xcode project, installs SafariWebExtensionHandler.swift, allows cleartext loopback networking, and builds a Release app into Xcode's DerivedData. It prints the path at the end.

2. Install exactly one copy

Move the app somewhere permanent before you open it:

mv ~/Downloads/"Hydra Safari Extension.app" /Applications/

Safari registers the extension from wherever the app currently sits, and deleting or moving the app afterwards uninstalls the extension.

Keep exactly one copy on disk. This matters more than it sounds. macOS registers every copy it finds — including one left in ~/Downloads and one in /Applications — and Safari then lists the same extension twice and refuses to enable either, because two apps claim the bundle id io.github.ja7ad.hydra.safari.Extension. The checkbox simply will not stick. Check what is registered:

pluginkit -mAv | grep hydra.safari

One line is correct. If you see more, delete the extra copies and unregister them:

/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -u ~/Downloads/"Hydra Safari Extension.app"

Then open the app once — double-click it. It shows a small window saying the extension is currently off. That window is the registration; there is nothing to configure in it.

3. Turn on Safari's developer features

Safari → Settings → Advanced → tick Show features for web developers (older macOS calls it Show Develop menu in menu bar).

4. Allow unsigned extensions — mind the order

Develop → Allow Unsigned Extensions.

Safari clears this setting every time it quits, and an unsigned extension goes back to disabled with it. So the order has to be:

  1. Quit Safari completely (⌘Q).
  2. Launch Safari.
  3. Develop → Allow Unsigned Extensions.
  4. Enable the extension (next step) — without quitting Safari in between.

The trap: the wrapper app's own "Quit and Open Safari Extensions Preferences…" button quits Safari on its way to the settings window, which clears the flag you just set. Use the Safari menu bar, not that button.

You repeat step 3 after every Safari restart. That is inherent to an unsigned build, not a fault in this one.

5. Enable it and grant access

Safari → Settings → Extensions → tick Hydra Download Manager Integration. There should be exactly one entry.

Then click the Hydra button in the toolbar and choose Always Allow on Every Website. Without host access the content script never runs, so the right-click item and the selection pill do nothing.

6. Check that it reached Hydra

The extension talks to Hydra over a plain WebSocket on loopback — port 6799, falling back to 16799. Open the popup: the status dot is green once the socket is up.

From the app's side, Hydra logs every connection and every refusal:

tail -f ~/.config/hydra/logs/gui.log | grep --line-buffered extbus

A healthy connection looks like:

[INFO ] extbus: ws connected (safari-web-extension://XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)

A refused one names the origin it turned away:

[WARN ] extbus: rejected ws handshake (origin: "…")

If neither line ever appears when you open the popup, the extension is not reaching the app at all — nothing arrived at the socket. Start at the extension isn't running below rather than looking at Hydra.

Troubleshooting

Two identical entries in Settings → Extensions, and the checkbox will not stay ticked

Two copies of the app are registered. See step 2 — delete the spare, lsregister -u it, quit Safari, relaunch, re-arm Allow Unsigned Extensions, tick.

The extension disappears, or disables itself, after every Safari restart

Expected for an unsigned build: Allow Unsigned Extensions is cleared on quit. Re-arm it, in a Safari session you have not quit since.

The extension is not in Settings → Extensions at all

Safari only scans for app extensions at launch, and you probably installed the app while it was running. Quit Safari with ⌘Q and reopen it. If it is still missing, confirm macOS knows about the extension with pluginkit -mAv | grep hydra.safari; an empty result means the app has not been opened yet, or it is in a location macOS does not scan.

"Hydra unreachable"

The popup could not reach the app. In order of likelihood:

  1. Hydra is not running. On Safari the extension cannot start it — see Known limits. Launch Hydra and try again.
  2. Nothing is listening. Check:
    lsof -nP -iTCP:6799 -sTCP:LISTEN
  3. The handshake was refused. Look for rejected ws handshake in ~/.config/hydra/logs/gui.log. Hydra accepts chrome-extension://, moz-extension:// and safari-web-extension:// origins; anything else is turned away by design.
  4. Nothing arrived at all — no log line either way. Then the extension's background never opened the socket; carry on below.

The popup does not open, and nothing reaches Hydra

Both symptoms together mean the extension's own pages are not running, which is a Safari-side failure rather than anything Hydra can see. Look at the background directly:

Safari → Develop → Web Extension Background Content → Hydra Download Manager Integration.

  • If that menu entry is missing, the background service worker never registered — that is the failure, and the console of the app's own window will not tell you more. Re-check steps 2 to 4.
  • If it is there, it opens a Web Inspector on the background. Any exception thrown while the background loads shows in its console, and that error is the thing to report.

Please open an issue with that console output if you hit this — it is the one piece of information that cannot be read from outside Safari.

Known limits

Three of these are properties of an unsigned developer build, not bugs:

  • No automatic download capture, ever. Safari has no downloads API. Use right-click or the selection pill.
  • Developer mode must be re-armed after every Safari restart. Signing the app with an Apple Developer ID and notarizing it is what removes this step.
  • Hydra cannot be launched by the extension. On Chrome and Firefox, the native-messaging host starts Hydra when it is not running. The Safari equivalent, SafariWebExtensionHandler, is blocked by the app sandbox from reading ~/.config/hydra/ipc.json and from launching the app, so on Safari the extension only works with Hydra already running. Everything the WebSocket carries — downloads, status, the popup — is unaffected.
  • Media sniffing is partial, limited to what Safari exposes through webRequest.

Uninstall

Quit Safari, then:

rm -rf /Applications/"Hydra Safari Extension.app"

The extension disappears from Safari with the app. To clear the registration immediately rather than at the next system scan:

/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -u /Applications/"Hydra Safari Extension.app"

For contributors

Edit extensions/chrome/*, never extensions/safari/Resources/* — the latter is regenerated from the Chrome sources by scripts/sync-extension-resources.sh safari on every build, and CI fails if the committed tree does not match what that script produces. The Safari manifest (extensions/safari/manifest.json) and SafariWebExtensionHandler.swift are Safari's own and are edited in place.

Clone this wiki locally