Shipping a Native Host with an Installer

Distribute a native messaging host to real users: per-user versus system installs, MSI and pkg installers, registry and manifest registration, code signing, updates, uninstall and onboarding from the extension.

Published October 2, 2026 Updated October 2, 2026 8 min read
Table of Contents

The extension is in the store and installs in one click. The native host it depends on is a zip file with a README that says “run register.sh”. Half the users who install the extension never get the native half working, and the support inbox fills with “Specified native messaging host not found”. The browser cannot install a host for you — no store will ship an executable — so the installer is part of the product, and its quality decides whether the feature exists for most users at all. This guide covers building one. It belongs to native messaging and host integration.

Why the host needs a real installer

A host installation has four parts that must all be right at once: an executable in a stable location the user cannot accidentally move, a host manifest per browser family with the correct absolute path baked in, a registration (registry key on Windows, manifest file in the right directory elsewhere) for every browser the user has, and OS trust — a signature, and on macOS notarisation — so the executable runs when the browser launches it. Each part varies by OS, and each must be undone cleanly on uninstall and replaced atomically on upgrade. A script can do it; an installer does it in a way users already know how to run and IT departments know how to deploy.

What a host install puts on diskThe signed executable, per-browser host manifests, registry keys or directory entries pointing at them, and an uninstall record.Signed executablestable path, executable bitGatekeeper / SmartScreenHost manifestschromium + firefox variantsabsolute path insideRegistrationregistry keys / NativeMessagingHosts dirsper browserUninstall recordremoves all of the aboveno orphans
Each layer has its own failure mode; an installer handles all four as one transaction.

Step-by-step: build and ship the installer

1. Choose per-user or system-wide

Per-user installs write to the user’s profile and HKCU, need no administrator rights, and are what consumer products should use. System-wide installs write to /Library, /etc/opt or HKLM, need elevation, and are what enterprises want because one install serves every account. Many products ship both: a per-user default and a system-wide option in the MSI or pkg for managed deployment.

1                     per-user                         system-wide
2Windows exe     %LOCALAPPDATA%\Acme\Vault\          %ProgramFiles%\Acme\Vault\
3Windows reg     HKCU\Software\…\NativeMessagingHosts  HKLM\Software\…\NativeMessagingHosts
4macOS manifest  ~/Library/Application Support/…      /Library/Google/Chrome/…
5Linux manifest  ~/.config/google-chrome/…            /etc/opt/chrome/native-messaging-hosts/

Execution context: installer design, before writing any tooling. A per-user HKCU key or home-directory manifest takes precedence over the system one, which lets a user-level install override an enterprise one — make sure your support docs mention it.

2. Generate manifests at install time, not build time

The manifest’s path must be the absolute path where the executable actually landed, which only the installer knows.

1<!-- WiX v4: write the Chrome manifest with the resolved install path -->
2<Component Id="ChromeManifest" Directory="INSTALLFOLDER">
3  <File Id="ManifestTemplate" Source="com.acme.vault.json" />
4  <util:JsonFile ... />  <!-- or a custom action that substitutes [INSTALLFOLDER] -->
5  <RegistryValue Root="HKCU"
6    Key="Software\Google\Chrome\NativeMessagingHosts\com.acme.vault"
7    Type="string" Value="[INSTALLFOLDER]com.acme.vault.json" KeyPath="yes" />
8</Component>

Execution context: the Windows installer. On Windows the manifest’s path may be relative to the manifest file, which avoids substitution if the JSON sits beside the executable — "path": "vault-host.exe". The registry value still needs the full path to the JSON. Add one RegistryValue per browser (Chrome, Edge, Mozilla), and because they are part of the component, uninstall removes them automatically.

3. Build a macOS pkg with a postinstall script

 1# scripts/postinstall — runs as root after the payload is copied
 2#!/bin/bash
 3HOST="/Applications/Acme Vault.app/Contents/MacOS/vault-host"
 4USER_HOME=$(dscl . -read "/Users/$USER" NFSHomeDirectory | awk '{print $2}')
 5for dir in "Google/Chrome" "Microsoft Edge" "BraveSoftware/Brave-Browser"; do
 6  d="$USER_HOME/Library/Application Support/$dir/NativeMessagingHosts"
 7  sudo -u "$USER" mkdir -p "$d"
 8  sudo -u "$USER" sed "s#__HOST__#$HOST#" /tmp/acme/chromium.json > "$d/com.acme.vault.json"
 9done
10exit 0

Execution context: the macOS Installer’s postinstall phase, running as root with $USER set to the installing user. Writing into the user’s Library as root would leave root-owned files the browser cannot read, hence sudo -u. For system-wide installs, write to /Library/Google/Chrome/NativeMessagingHosts/ instead and skip the user loop. Build with pkgbuild --scripts scripts --root payload and productbuild, then sign with a Developer ID Installer certificate.

From build artefact to working hostBuild the executable, sign and notarise it, package it with manifest templates, install to a fixed path, write manifests with the real path, register per browser, and smoke-test from the extension.Build hostper OS + archSignAuthenticode / Developer IDPackageMSI · pkg · debon the user's machineInstallfixed pathRegistermanifests + keysSmoke testhello from extension
Signing happens before packaging; registration happens at install time on the user's machine.

4. Sign everything the OS will check

1# macOS: sign the binary with hardened runtime, then notarise the pkg
2codesign --force --options runtime --timestamp \
3  --sign "Developer ID Application: Acme Inc (TEAMID)" "payload/Acme Vault.app/Contents/MacOS/vault-host"
4productsign --sign "Developer ID Installer: Acme Inc (TEAMID)" unsigned.pkg AcmeVault.pkg
5xcrun notarytool submit AcmeVault.pkg --keychain-profile acme --wait
6xcrun stapler staple AcmeVault.pkg

Execution context: your release machine or CI runner with the signing identities. An unsigned or un-notarised host launched by Chrome is blocked by Gatekeeper silently — the extension sees “Native host has exited” and the user sees nothing. On Windows, Authenticode-sign both the executable and the MSI; unsigned executables trigger SmartScreen warnings at install and are blocked outright by many corporate policies.

5. Detect the host from the extension and guide the user

1// popup or first-run page
2const status = await chrome.runtime.sendMessage({ type: "host:status" });
3if (!status.installed) {
4  const os = (await chrome.runtime.getPlatformInfo()).os;   // "win" | "mac" | "linux" | …
5  showInstallCard({
6    href: `https://acme.example/download/vault-host?os=${os}`,
7    reason: status.reason,                                  // "not-installed" | "host-crashed" …
8  });
9}

Execution context: an extension page asking the service worker, which runs the hello probe described in the native messaging and host integration overview. chrome.runtime.getPlatformInfo gives a reliable OS without parsing the user agent. After the user installs, offer a “Check again” button rather than polling — the probe launches a process each time.

6. Handle upgrades and uninstalls

Upgrades must replace the executable while the browser may have it running. On Windows, MSI’s restart manager will ask to close the browser; a gentler approach is to install to a versioned directory, rewrite the manifest to point at it, and delete the old version on next launch. On macOS, replacing an app bundle while its helper runs works, because the running process keeps the old file open. Uninstall must remove every manifest and registry key the installer wrote — an orphaned manifest pointing at a deleted executable converts “not found”, which your onboarding handles, into “has exited”, which it may not.

1// sw.js — surface a version mismatch after an extension update
2const hello = await chrome.runtime.sendNativeMessage("com.acme.vault", { type: "hello" });
3if (hello.protocol < MIN_HOST_PROTOCOL) notifyUpdateHost(hello.version);

Execution context: the service worker, on startup or before the first feature use. The extension updates itself; the host does not. Telling the user precisely which version they have and which they need turns a confusing failure into a one-click fix. Firefox and Safari need the same check — Safari’s “host” updates with the containing app through the App Store, which narrows but does not close the gap.

Packaging formats by platformRecommended installer format, signing requirement and enterprise deployment route for a native host on Windows, macOS and Linux.PlatformFormatSigningEnterprise routeWindowsMSI (WiX)AuthenticodeIntune / GPOmacOSSigned pkgDeveloper ID + notariseMDM (Jamf, Kandji)Linuxdeb / rpmRepo signing keyConfig managementSafariApp Store appApp StoreMDM app install
Use the format each platform's administrators already deploy.

Cross-browser variation

  • Chrome / Edge / Brave: one Chromium manifest template, written once per browser directory or registry key. Edge on Windows reads its own Microsoft\Edge key — do not rely on it falling back to Chrome’s.
  • Firefox: a second template with allowed_extensions; registry key under Mozilla. Firefox for Android has no native messaging.
  • Safari: no installer of this kind — the host ships inside the app you distribute through the Mac App Store or as a notarised download, and installing the app is the whole installation.

Verification

  1. On a clean virtual machine for each OS, install the extension from the store and confirm it shows the install card.
  2. Run the installer as a standard (non-admin) user and click “Check again” — the card should disappear and the feature should work.
  3. Run the uninstaller and confirm every manifest and registry key is gone: reg query HKCU\Software\Google\Chrome\NativeMessagingHosts on Windows, ls ~/Library/Application\ Support/Google/Chrome/NativeMessagingHosts/ on macOS.
  4. Install an older host version and confirm the extension reports “update required” rather than failing.

FAQ

Can the extension download and run the installer itself?

It can download the file with chrome.downloads, but it cannot execute it; the user must open it. Store policies also forbid extensions that install software without clear user consent, so make the download an explicit user action with a clear explanation.

Should the host auto-update itself?

If it ships outside an app store, yes — or users will run ancient versions indefinitely. Keep the updater separate from the host process the browser launches, so an update never interrupts a live connection.

What if the user has no admin rights?

That is the case per-user installs exist for. Default to per-user and make system-wide an option administrators choose.

Other Core APIs & Cross-Browser Data Management Resources