How Chromium Native Messaging Connects Extensions to Apps

This article provides a comprehensive overview of Native Messaging in the Chromium web browser. It explains the core architecture of this feature, the security registration process required to link a browser extension with a local desktop application, and the exact mechanics of how data is formatted and exchanged between the two using standard input (stdin) and standard output (stdout) streams.

What is Native Messaging?

Chromium extensions run in a secure, sandboxed environment with no direct access to the user's underlying operating system or local file system. Native Messaging is a specialized Chromium API that bridges this gap. It allows a browser extension to communicate directly with a separately installed local desktop application—referred to as the "native messaging host"—without exposing the browser to arbitrary system-level vulnerabilities.

Prerequisite Configuration

Before any data can be exchanged, both the extension and the local desktop application must be explicitly configured to trust one another.

  1. The Extension Manifest: The Chromium extension must request the "nativeMessaging" permission within its manifest.json file.
  2. The Host Manifest: The local desktop application must include its own manifest file (also in JSON format). This file defines a unique name for the host, the absolute file path to the local executable, the communication type (which must be set to "stdio"), and an "allowed_origins" array. The "allowed_origins" property is a critical security measure that lists the exact Chromium Extension IDs permitted to communicate with the local application.
  3. OS Registration: The host manifest file must be registered at the operating system level. On Windows, this is done by adding a specific key to the Windows Registry that points to the host manifest. On macOS and Linux, the manifest file must be placed in a specific, predetermined system directory.

Initiating the Connection

The local application cannot initiate a connection with the browser; the Chromium extension must always start the session. The extension does this by calling one of two API methods, passing the registered name of the native host:

  • chrome.runtime.connectNative(): Starts a persistent, long-lived connection that allows multiple messages to be sent back and forth.
  • chrome.runtime.sendNativeMessage(): Starts a one-time connection, sends a single message, waits for a single response, and then closes the application.

When the extension calls either of these methods, Chromium reads the OS-registered host manifest, verifies that the extension's ID matches the "allowed_origins" list, and launches the local desktop application as a separate background process.

Exchanging Data via Standard Streams

Once Chromium launches the native application, it does not use network sockets or HTTP to communicate. Instead, Chromium binds directly to the application's standard input (stdin) and standard output (stdout) streams. Chromium acts as a translator, converting JavaScript API calls from the extension into stream writes, and stream reads back into JavaScript events.

  • Extension to Local App (stdin): When the extension sends a message (using postMessage), Chromium takes the JavaScript object, serializes it into a UTF-8 encoded JSON string, and writes it directly to the local application's stdin stream. The local application reads from its stdin to process the incoming data.
  • Local App to Extension (stdout): When the local application wants to send data back to the extension, it formats the data as a JSON string and writes it to its stdout stream. Chromium continuously listens to the application's stdout, reads the incoming data, parses it back into a JavaScript object, and triggers the onMessage event listener inside the extension.

The Data Wire Protocol

Because stdin and stdout are continuous streams of bytes rather than discrete packets, both Chromium and the local application need a way to know where one JSON message ends and the next one begins. To solve this, Native Messaging enforces a strict wire protocol for every message sent in either direction:

  1. The Length Prefix: Every message must be preceded by a 32-bit (4-byte) integer in the host computer's native byte order (usually little-endian). This integer indicates the exact byte length of the upcoming JSON message.
  2. The Payload: Immediately following the 4-byte integer is the UTF-8 encoded JSON string itself.

When the local application reads from stdin, it must first read exactly 4 bytes, convert those bytes into an integer \(N\), and then read the next \(N\) bytes to get the complete JSON payload. Conversely, before the local application writes a JSON string to stdout, it must calculate the byte length of that string, write the 4-byte integer to stdout, and then write the string itself.

Operational Constraints and Debugging

Because stdout is strictly reserved for the 4-byte header and JSON payload protocol, the local desktop application must never use standard print statements (like printf or console.log) for debugging. If the application writes unformatted text to stdout, Chromium will attempt to read the first 4 characters as a message length, fail to parse the data, and immediately terminate the native application process.

If the local application needs to output logs or errors, it must write them to the standard error (stderr) stream, which Chromium safely ignores or routes to the browser's main error console. Finally, Chromium enforces size limits on the streams: a single message sent from the extension to the host via stdin can be up to 4 GB, while a message sent from the host to the extension via stdout cannot exceed 1 MB.