Rust helpers for building native messaging hosts for Chrome/Chromium and Firefox. This crate handles:
- Writing browser-correct host manifests (Chrome
allowed_origins, Firefoxallowed_extensions) - Installing at user or system scope (with Windows registry handling for Chrome)
- Verifying/removing manifests
- Correct message framing (4-byte length prefix + UTF-8 JSON) with async helpers to read, write, and loop
Build the parts that matter—leave the manifest details and wire protocol to us.
Make the native host side of WebExtensions native messaging boringly correct and portable:
- Zero boilerplate for manifests: Generate the right shape for each browser, write to the correct OS paths, and (on Windows) handle the Chrome registry.
- Correct-by-default protocol: Enforce the 4-byte length prefix + UTF-8 JSON framing and sensible size limits.
- Ergonomic async I/O: Small, focused helpers (
get_message,send_message,event_loop) so you can ship a host quickly. - Testability: Functions are easy to unit/integration test (e.g., in-memory framing, temp HOME paths).
Purpose: This crate is for the native host (app) side of WebExtensions native messaging. It gives you:
- Host manifest creation/installation/removal for Chrome & Firefox
- Host-side message framing (length-prefixed JSON) and async I/O helpers
Not included: It does not implement the extension (browser) side. You’ll still write normal Chrome/Firefox extension code (connectNative / sendNativeMessage). The README includes minimal extension snippets purely to help you test your host.
- ✅ Cross-browser: Generates separate host manifests for Chrome and Firefox with the right keys.
- ✅ Cross-platform: Linux, macOS, and Windows (Chrome registry supported).
- ✅ Scopes: Write to user or system locations (system may require elevated privileges).
- ✅ Protocol helpers: Encode/decode frames; async
get_message,send_message, andevent_loop.
| OS | Chrome/Chromium | Firefox | Notes |
|---|---|---|---|
| Linux | ✅ | ✅ | Absolute path required in manifests |
| macOS | ✅ | ✅ | Absolute path required in manifests |
| Windows | ✅ | ✅ | Chrome requires a registry value pointing to the manifest file |
Add to your Cargo.toml:
[dependencies]
native_messaging = "0.1.2"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }use std::path::Path;
use native_messaging::install::manifest::{install, Browser, Scope};
fn main() -> std::io::Result<()> {
// 1) Absolute path to your host executable (required on macOS/Linux)
let host_exe = if cfg!(windows) {
Path::new(r"C:\full\path\to\your_host.exe")
} else {
Path::new("/full/path/to/your_host")
};
// 2) Chrome requires chrome-extension://<ID>/ origins
// (get the ID from chrome://extensions after loading your extension)
let chrome_origin = "chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/".to_string();
// 3) Firefox requires add-on IDs (set in manifest via browser_specific_settings.gecko.id)
let firefox_id = "native-test@example.com".to_string();
// 4) Install both manifests at user scope
install(
"com.example.native_host", // host name used by the extension
"Example native host", // description
host_exe, // absolute path to host
&[chrome_origin], // Chrome allowed_origins
&[firefox_id], // Firefox allowed_extensions
&[Browser::Chrome, Browser::Firefox],
Scope::User,
)
}use native_messaging::install::manifest::{verify, remove, Browser, Scope};
fn main() -> std::io::Result<()> {
// Check if either Chrome/Firefox user-scope manifest exists
let present = verify("com.example.native_host")?;
println!("present? {present}");
// Remove both manifests (also removes Chrome registry value on Windows)
remove("com.example.native_host",
&[Browser::Chrome, Browser::Firefox],
Scope::User)?;
Ok(())
}Hosts talk to the browser via length-prefixed JSON. Use the helpers below.
Read one message from stdin:
use native_messaging::host::get_message;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let msg = get_message().await?;
eprintln!("got: {msg}");
Ok(())
}Send one JSON message to stdout:
use native_messaging::host::send_message;
use serde::Serialize;
#[derive(Serialize)]
struct Greeting { hello: String }
#[tokio::main]
async fn main() -> std::io::Result<()> {
let g = Greeting { hello: "from host".into() };
send_message(&g).await
}Run an async event loop:
use native_messaging::host::{event_loop, send_message};
use serde_json::json;
use std::io;
async fn handle_message(message: String) -> io::Result<()> {
// echo back in a wrapper JSON
let reply = json!({ "echo": message });
send_message(&reply).await
}
#[tokio::main]
async fn main() -> io::Result<()> {
event_loop(handle_message).await
}-
install(name, description, exe_path, chrome_allowed_origins, firefox_allowed_extensions, browsers, scope)Writes the correct manifests for requested browsers. On Windows+Chrome, also writes the registry value pointing at the Chrome manifest file. -
verify(name) -> io::Result<bool>Returnstrueif user-scope Chrome or Firefox manifest exists. -
remove(name, browsers, scope)Deletes the manifest files for the given browsers and scope. Also removes Chrome’s registry value on Windows.
Types
enum Browser { Chrome, Firefox }enum Scope { User, System }
⚠️ On macOS/Linux,exe_pathmust be absolute. On Windows, absolute paths are strongly recommended (and used by default in examples).
-
encode_message<T: Serialize>(&T) -> io::Result<Vec<u8>>Build a framed message (len:u32+ JSON bytes). Enforces a 1 MB limit host→browser. -
decode_message<R: Read>(&mut R, max_size: usize) -> io::Result<String>Read and decode one frame from a reader (defaults elsewhere to ≤64 MB browser→host). -
get_message() -> io::Result<String>(async) Read a single framed message from stdin. -
send_message<T: Serialize>(&T) -> io::Result<()>(async) Frame and write a JSON message to stdout. -
event_loop(handler) -> io::Result<()>(async) Callhandler(String)for each incoming message forever.
-
Chrome:
- Manifest key is
allowed_originswith entries likechrome-extension://<ID>/. - Your extension must declare
"permissions": ["nativeMessaging"]and callchrome.runtime.connectNative("com.example.native_host"). - MV3 service workers: a live native messaging port keeps the worker alive, but only while connected—handle
onDisconnectand reconnect as needed.
- Manifest key is
-
Firefox:
-
Manifest key is
allowed_extensionswith your add-on IDs. -
Set a stable ID in your extension’s
manifest.json:{ "browser_specific_settings": { "gecko": { "id": "native-test@example.com" } } } -
Use
browser.runtime.connectNative("com.example.native_host").
-
Protocol sanity (no browser):
use native_messaging::host::{encode_message, decode_message};
use serde_json::json;
fn main() -> std::io::Result<()> {
// Encode a message like the browser would
let frame = encode_message(&json!({"hello": "world"}))?;
// Read it back via an in-memory cursor (like host would do)
let mut cursor = std::io::Cursor::new(frame);
let s = decode_message(&mut cursor, 64 * 1024 * 1024)?; // 64MB cap
assert!(s.contains("hello"));
Ok(())
}End-to-end with a tiny host:
- Build a small echo host that reads with
get_message()and replies withsend_message(). - Install manifests (user scope).
- Load a minimal test extension in Chrome/Firefox and connect using
connectNative().
-
“Specified native messaging host not found”
- Host
namemismatch between extension & manifest - Manifest written to the wrong location (check OS paths)
- Windows + Chrome: missing registry value (re-run install on Windows)
exe_pathnot absolute (macOS/Linux)
- Host
-
Disconnects / No messages
- Host crashed: run the host binary manually to see stderr logs
- Message too large: host enforces 1 MB host→browser
- Chrome MV3: service worker stopped—reconnect on
onDisconnect
-
Multiple Chrome channels/profiles
- Use stable Chrome and the default profile for first validation; paths vary per channel/profile.
Contributions welcome!
- Fork
- Branch (
feature/my-feature) - Commit (
feat: add X) - PR
Please follow the Contributor Covenant.
MIT — see LICENSE.
Build great WebExtensions. We’ll handle the plumbing.