Registering Native Hosts on Windows, macOS and Linux
Where Chrome, Edge, Brave and Firefox look for native messaging host manifests on Windows, macOS and Linux — registry keys, per-user and system directories, and a script that registers all of them.
Table of Contents
chrome.runtime.connectNative("com.acme.vault") fails with “Specified native messaging host not found”, even though the host manifest is sitting right there on disk. It is in the wrong place — or the right place for a different browser. Every Chromium-based browser reads its own directory, Firefox reads another, Windows uses the registry instead of directories, and per-user and system-wide locations differ again. This guide lists them and builds a registration script that covers the browsers your users actually run. It belongs to native messaging and host integration.
How the browser finds a host
When an extension calls connectNative(name), the browser looks up a file named <name>.json — or, on Windows, a registry key named <name> — in a fixed list of locations, checking per-user before system-wide. The first match wins. The file must be valid JSON, its name field must equal the requested name, its path must point at an existing executable, and its allowed_origins (or Firefox’s allowed_extensions) must include the caller. Each of those checks has its own failure, but “not found” specifically means the lookup step: no file or key with that name existed in any of the places this particular browser searches.
Step-by-step: register for every browser you support
1. Write the manifest once per browser family
Chromium browsers and Firefox disagree on one key, so generate two variants from a template.
1{
2 "name": "com.acme.vault",
3 "description": "Acme Vault native bridge",
4 "path": "/opt/acme-vault/vault-host",
5 "type": "stdio",
6 "allowed_origins": [
7 "chrome-extension://abcdefghijklmnopabcdefghijklmnop/"
8 ]
9}
Execution context: a file on disk read by Chromium-based browsers. name may contain only lower-case letters, digits, dots and underscores, and may not start or end with a dot. Each origin in allowed_origins needs the trailing slash and no wildcards. For Firefox, replace allowed_origins with "allowed_extensions": ["vault@acme.example"], using the id from browser_specific_settings.gecko.id.
2. Know the per-user locations
1macOS
2 Chrome ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/
3 Chromium ~/Library/Application Support/Chromium/NativeMessagingHosts/
4 Edge ~/Library/Application Support/Microsoft Edge/NativeMessagingHosts/
5 Brave ~/Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts/
6 Firefox ~/Library/Application Support/Mozilla/NativeMessagingHosts/
7
8Linux
9 Chrome ~/.config/google-chrome/NativeMessagingHosts/
10 Chromium ~/.config/chromium/NativeMessagingHosts/
11 Edge ~/.config/microsoft-edge/NativeMessagingHosts/
12 Brave ~/.config/BraveSoftware/Brave-Browser/NativeMessagingHosts/
13 Firefox ~/.mozilla/native-messaging-hosts/
Execution context: the user’s home directory, writable without elevation. Beta, Dev and Canary channels of Chrome use their own directories (Google/Chrome Beta, google-chrome-beta), which surprises developers who test on Canary. Flatpak and Snap browsers on Linux run sandboxed and cannot see these directories at all without extra configuration.
3. Know the system-wide locations
1macOS
2 Chrome /Library/Google/Chrome/NativeMessagingHosts/
3 Edge /Library/Microsoft/Edge/NativeMessagingHosts/
4 Firefox /Library/Application Support/Mozilla/NativeMessagingHosts/
5
6Linux
7 Chrome /etc/opt/chrome/native-messaging-hosts/
8 Chromium /etc/chromium/native-messaging-hosts/
9 Edge /etc/opt/edge/native-messaging-hosts/
10 Firefox /usr/lib/mozilla/native-messaging-hosts/ (or /usr/lib64/…)
Execution context: system directories requiring root or admin, used by package installers and enterprise deployment tools. A per-user manifest with the same name takes precedence, which is useful for developers overriding an installed host and confusing for support when an old per-user file shadows a newly installed system one.
4. Register on Windows through the registry
1# Per-user, Chrome and Edge, PowerShell
2$manifest = "$env:LOCALAPPDATA\AcmeVault\com.acme.vault.json"
3foreach ($key in @(
4 "HKCU:\Software\Google\Chrome\NativeMessagingHosts\com.acme.vault",
5 "HKCU:\Software\Microsoft\Edge\NativeMessagingHosts\com.acme.vault",
6 "HKCU:\Software\Mozilla\NativeMessagingHosts\com.acme.vault"
7)) {
8 New-Item -Path $key -Force | Out-Null
9 Set-ItemProperty -Path $key -Name "(default)" -Value $manifest
10}
Execution context: PowerShell as the user. The key’s default value is the full path to the JSON file — not the JSON itself, and not a named value. Firefox’s key should point at the Firefox variant of the manifest. In the manifest, path may be relative to the JSON file’s directory on Windows only. 32-bit browsers on 64-bit Windows read the WOW6432Node view of HKLM; per-user HKCU keys avoid that complication entirely.
5. Script the whole thing on macOS and Linux
1#!/usr/bin/env bash
2set -euo pipefail
3NAME=com.acme.vault
4HOST_PATH="$1" # absolute path to the executable
5CHROME_ID="abcdefghijklmnopabcdefghijklmnop"
6GECKO_ID="vault@acme.example"
7
8chromium_json=$(printf '{"name":"%s","description":"Acme Vault","path":"%s","type":"stdio","allowed_origins":["chrome-extension://%s/"]}' "$NAME" "$HOST_PATH" "$CHROME_ID")
9firefox_json=$(printf '{"name":"%s","description":"Acme Vault","path":"%s","type":"stdio","allowed_extensions":["%s"]}' "$NAME" "$HOST_PATH" "$GECKO_ID")
10
11if [[ "$(uname)" == "Darwin" ]]; then
12 base="$HOME/Library/Application Support"
13 chromium_dirs=("$base/Google/Chrome" "$base/Chromium" "$base/Microsoft Edge" "$base/BraveSoftware/Brave-Browser")
14 firefox_dir="$base/Mozilla/NativeMessagingHosts"
15else
16 chromium_dirs=("$HOME/.config/google-chrome" "$HOME/.config/chromium" "$HOME/.config/microsoft-edge" "$HOME/.config/BraveSoftware/Brave-Browser")
17 firefox_dir="$HOME/.mozilla/native-messaging-hosts"
18fi
19
20for d in "${chromium_dirs[@]}"; do
21 [[ -d "$d" ]] || continue # only browsers that are installed
22 mkdir -p "$d/NativeMessagingHosts"
23 echo "$chromium_json" > "$d/NativeMessagingHosts/$NAME.json"
24done
25mkdir -p "$firefox_dir" && echo "$firefox_json" > "$firefox_dir/$NAME.json"
26chmod +x "$HOST_PATH"
Execution context: a shell as the user, typically run by an installer’s post-install step. Registering only for browsers whose profile directory exists avoids littering the home directory; re-run the script after the user installs a new browser, or register for every known browser unconditionally if your support burden favours that. The chmod matters: a manifest pointing at a non-executable file produces “Native host has exited”, not “not found”.
6. Keep a separate registration for development
An unpacked extension has a different id from the store build unless you pin it with the key field, and a host built from source lives in your checkout rather than in /opt. Rather than editing the production manifest, register a second host name for development and choose between them at build time.
1// build-time constant injected by the bundler
2const HOST_NAME = import.meta.env.DEV ? "com.acme.vault.dev" : "com.acme.vault";
3export const connectHost = () => chrome.runtime.connectNative(HOST_NAME);
Execution context: the service worker, compiled per environment. The development manifest (com.acme.vault.dev.json) points at a wrapper script in your checkout and lists the unpacked extension’s origin. Because the names differ, a developer can have the production host installed from the real installer and the development host registered from source on the same machine without either shadowing the other — which also means you can test the production installer without uninstalling your working setup.
Linux sandboxed browsers deserve a special note here. A Chrome or Firefox installed as a Flatpak or Snap cannot see host manifests in the usual home-directory locations and cannot launch executables outside its sandbox without additional portal configuration. If your users report “not found” on Linux and everything looks correct, ask how the browser was installed; the honest answer for a sandboxed package is usually to recommend the distribution’s native package or the vendor’s own .deb/.rpm.
Cross-browser variation
- Chrome / Edge / Brave / Chromium: identical manifest format with
allowed_origins; each reads only its own directory or registry key. Opera and Vivaldi read Chrome’s locations on some platforms and their own on others — test them explicitly if you support them. - Firefox:
allowed_extensionswith add-on ids; separate registry key and directories. Firefox ESR uses the same locations as release. - Safari: no registration. The host is the macOS app that contains the Safari extension; installing the app is registering the host.
Verification
- Confirm the file exists where the browser looks:
ls "$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts/". - Validate the JSON:
python3 -m json.tool < com.acme.vault.json. - Confirm the executable runs on its own:
"$HOST_PATH" < /dev/null; echo $?should print an exit code of 0, not “permission denied”. - From the extension’s service worker console:
1await chrome.runtime.sendNativeMessage("com.acme.vault", { type: "hello" });
2// → { protocol: 3, version: "2.0.0", features: [...] }
Execution context: the service worker console. A resolved reply proves lookup, authorisation, launch and framing all work. On Windows, reg query "HKCU\Software\Google\Chrome\NativeMessagingHosts\com.acme.vault" confirms the key and its default value.
FAQ
Do I need a separate manifest for each Chrome channel?
The file can be identical, but it must exist in each channel’s directory — Chrome Beta, Dev and Canary each read their own. Developers testing on Canary often register only for stable and wonder why lookup fails.
Can the manifest point at a script instead of a binary?
On macOS and Linux, yes, if it has a shebang and the executable bit; the browser launches it with a minimal environment, so absolute interpreter paths are safest. On Windows, point at a .bat or .exe; a .bat wrapper is common in development and slow in production.
How do enterprises deploy hosts?
With system-wide locations written by their software distribution tool, often alongside the NativeMessagingAllowlist policy, which restricts which host names extensions may connect to at all.
Related
- Shipping a native host with an installer — wrapping this script in a real installer.
- Debugging “native host has exited” — what to check once lookup succeeds.
- Pinning a stable extension id with the key field — keeping
allowed_originsvalid in development. - Native messaging and host integration — the parent topic.