Skip to main content
Native messaging lets your extension exchange JSON messages with an application on the user’s machine. The browser starts the application as a child process and pipes messages over stdin and stdout. Use it for work the extension platform cannot do: read local files, talk to hardware, or call a password manager. Three pieces have to agree:
  1. The extension declares the nativeMessaging permission.
  2. A host manifest, a small JSON file registered with the OS, names the application and the extensions that may call it.
  3. The application speaks the length-prefixed stdio protocol.

Declare the permission

Add nativeMessaging to the permissions in your manifest.json:
Extension.js writes the permission into the built manifest unchanged. There is no build-time handling beyond that: the host itself is never bundled.

Write the host manifest

The host manifest tells the browser where the application lives and who may start it:
  • name is the identifier that your extension passes to connectNative(). Use lowercase letters, digits, underscores, and dots.
  • path must be absolute on macOS and Linux. On Windows it may be relative to the manifest, and it must point to an executable (use a .bat wrapper for a Node.js script).
  • type is always stdio.
  • allowed_origins lists the extension IDs that may call this host. Read your ID from chrome://extensions with Developer mode on.

Where to install it

Chromium browsers find the manifest in fixed per-OS locations, named after the host (com.example.ping.json): Chromium (the open-source build) uses Chromium instead of Google/Chrome on macOS, and ~/.config/chromium/NativeMessagingHosts/ plus /etc/chromium/native-messaging-hosts/ on Linux. The per-user directory on macOS and Linux lives inside the browser’s user data directory, which matters during extension dev (see below).

A minimal Node.js host

Each message is a 32-bit unsigned integer length, in native byte order (little-endian on every supported platform), followed by that many bytes of UTF-8 JSON. The same framing applies in both directions. This host echoes every message back:
ping-host.js
On macOS and Linux, point path at an executable wrapper script and make it executable:
ping-host
Never write logs to stdout: the browser parses everything on that stream as message frames. Log to stderr or a file instead.

Connect from the background script

Use a port for an ongoing conversation. The browser starts the host process on connectNative() and stops it when the port disconnects:
background.js
For a single request and response, sendNativeMessage() starts a fresh host process per call:
Messages from the host to the extension are capped at 1 MB. Messages from the extension to the host are capped at 4 GB. Reloading the extension disconnects every open port, so reconnect in your startup path if the connection must survive.

Firefox differences

Firefox uses the same permission, the same APIs (browser.runtime.connectNative), and the same stdio protocol. The registration side differs:
  • Your extension needs an explicit ID through browser_specific_settings.gecko.id. Use the firefox: manifest prefix so the key only reaches Firefox builds.
  • The host manifest replaces allowed_origins with allowed_extensions, listing that ID.
Firefox looks in its own locations, independent of the Firefox profile:

Native messaging during extension dev

The host is registered with the OS, not bundled with your code. extension dev neither copies nor registers anything, so a host that works in development behaves the same after a store install. One profile detail matters on Chromium. By default, extension dev launches the browser with a fresh managed profile through --user-data-dir. Chromium resolves per-user host manifests inside the active user data directory, so a manifest that you installed for your everyday Chrome is invisible to that managed profile. Choose one of these setups:
  • Install the host manifest system wide (or, on Windows, in the registry). Those locations do not depend on the profile.
  • Run against your real browser profile instead:
Firefox and Windows lookups are per OS user, not per profile, so the managed dev profile reaches them with no extra steps.

Best practices

  • Validate every message: The host runs with the user’s full OS privileges, so treat extension input as untrusted and vice versa.
  • Keep stdout clean: One stray console.log in the host corrupts the frame stream.
  • Handle disconnects: Inspect chrome.runtime.lastError in onDisconnect to distinguish a missing host from a crashed one.
  • Prefer ports for repeated calls: sendNativeMessage() pays a process start per message.

Next steps