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.
- The Extension Manifest: The Chromium extension must
request the
"nativeMessaging"permission within itsmanifest.jsonfile. - 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. - 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 (usingpostMessage), Chromium takes the JavaScript object, serializes it into a UTF-8 encoded JSON string, and writes it directly to the local application'sstdinstream. The local application reads from itsstdinto 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 itsstdoutstream. Chromium continuously listens to the application'sstdout, reads the incoming data, parses it back into a JavaScript object, and triggers theonMessageevent 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:
- 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.
- 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.