Debugging "Native host has exited"

Diagnose the 'Native host has exited' native messaging error in Chrome and Firefox: run the host by hand, read its stderr, check permissions, runtimes, stdout pollution and early exits.

Published October 2, 2026 Updated October 2, 2026 7 min read
Table of Contents

The port disconnects the instant you open it and chrome.runtime.lastError.message reads “Native host has exited.” Nothing else. It is the most common native messaging error and the least informative, because it only says that the browser found your host, launched it, and then saw its stdout close. The cause could be a crash, a missing runtime, a file the host could not open, an executable bit that is not set, a single print statement, or a host that simply returned from main too early. This guide narrows it down methodically. It sits under native messaging and host integration.

What the error actually means

The browser’s native messaging client has three stages: find and validate the host manifest, launch the process, and exchange frames. “Not found” and “forbidden” are stage-one errors. “Native host has exited” is stage two or three: the process started (or the OS was asked to start it), and the read end of its stdout reached EOF before the browser expected. The browser does not distinguish between “the OS refused to run the file”, “the program crashed on line one”, and “the program ran perfectly and then exited” — all three close stdout. That is why the fix is always the same first step: take the browser out of the loop and run the host yourself, with the same arguments and the same environment the browser would use.

Where in the host's life the failure occursThe browser launches the process, the OS loads it, the runtime starts, the host reads its first frame and replies; failure at any point after launch produces the same error.connectNative()first replyManifest lookupnot found / forbiddenOS execpermission, quarantineRuntime startmissing Node/PythonHost initcrash, early returnFirst framestdout pollutionprocess launched"has exited" if stdout closes anywhere here
Every failure to the right of 'launch' looks identical from the extension.

Step-by-step diagnosis

1. Capture the exact error and when it happens

1// sw.js — temporary diagnostic
2const port = chrome.runtime.connectNative("com.acme.vault");
3const t0 = performance.now();
4port.onDisconnect.addListener(() => {
5  console.error("disconnect after", Math.round(performance.now() - t0), "ms:",
6    chrome.runtime.lastError?.message);
7});
8port.onMessage.addListener((m) => console.log("host said", m));
9port.postMessage({ type: "hello" });

Execution context: the service worker console. The time is a useful clue. A disconnect within a few milliseconds usually means the OS could not execute the file at all; tens to hundreds of milliseconds suggests the runtime started and then failed; a disconnect after a valid reply means the host exits once it has answered — fine for sendNativeMessage, wrong for a port. In Firefox read port.error instead of lastError.

2. Run the host exactly as the browser does

Read path from the host manifest and run that, with the arguments the browser passes and a framed message on stdin.

1HOST="/opt/acme-vault/vault-host"
2ORIGIN="chrome-extension://abcdefghijklmnopabcdefghijklmnop/"
3printf '\x11\x00\x00\x00{"type":"hello"}' | env -i HOME="$HOME" PATH="/usr/bin:/bin" "$HOST" "$ORIGIN"
4echo "exit code: $?"

Execution context: a terminal. env -i with a minimal PATH mimics the stripped environment browsers launch hosts with — the single most common difference between “works in my terminal” and “exits under Chrome”. The printf sends a correctly framed 17-byte hello. Watch what appears: an error message, a stack trace, or nothing at all followed by a non-zero exit code. On Windows, run the .exe from a plain cmd.exe and pipe a file containing the framed bytes.

3. Check that the OS will run the file

1ls -l "$HOST"                        # needs x permission for the user
2file "$HOST"                         # right architecture? arm64 vs x86_64
3xattr -l "$HOST"                     # macOS: com.apple.quarantine present?
4codesign --verify --verbose "$HOST"  # macOS: signature valid?
5head -1 "$HOST"                      # script? check the shebang

Execution context: a terminal on the user’s machine (or a support script that collects these). A missing execute bit, an x86-only binary on Apple Silicon without Rosetta, a quarantine attribute from a download, or an unsigned binary blocked by Gatekeeper all make the launch fail before any of your code runs. A script whose shebang reads #!/usr/bin/env node fails when the browser’s PATH does not contain Node — which on macOS, where GUI apps do not inherit shell profiles, is the normal case.

Clues that point to each causeTime to disconnect, behaviour when run by hand, and stderr output for each common cause of the 'Native host has exited' error.CauseDisconnect afterRun by handstderrNot executable / quarantined< 10 msPermission deniedNothingRuntime missing from PATH< 50 msWorks in shell, fails wit…env: node: not foundCrash during init50–500 msStack traceThe exceptionstdout pollutionAfter first writeText before frameOften cleanExits after replyAfter a valid replyReplies, then exits 0Clean
Running the host by hand with a minimal environment separates most causes in one try.

4. Read the host’s stderr from the browser

When the host works by hand but not under the browser, you need its stderr from the real launch.

1# Chrome on macOS: stderr from native hosts goes to Chrome's log
2/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
3  --enable-logging=stderr --v=1 2>&1 | grep -i -A3 "native"

Execution context: a terminal launching a fresh Chrome instance (quit Chrome first, or use a separate --user-data-dir). Host stderr lines appear interleaved with Chrome’s own log. In Firefox, host stderr appears in the Browser Console (Ctrl+Shift+J) without any flags. Alternatively, have the host write its own log file to a known location such as ~/Library/Logs/acme-vault-host.log — the most reliable option for collecting diagnostics from users.

5. Rule out stdout pollution

1printf '\x11\x00\x00\x00{"type":"hello"}' | "$HOST" "$ORIGIN" | xxd | head -3

Execution context: a terminal. The first four bytes of output must be a little-endian length and byte five must be {. If you see readable text first — a banner, a deprecation warning, “Listening…” — something wrote to stdout. The browser read those characters as a length of hundreds of millions, gave up, and closed the pipe; your host then died of EPIPE on its next write, which is why the error says “exited” rather than “communicating”. The full framing rules are in the native messaging wire format.

6. Check that the host stays alive for a port

1// Node host: keep reading until EOF — never exit after the first reply
2process.stdin.on("end", () => process.exit(0));
3// WRONG: handle(msg); process.exit(0);

Execution context: the host process. Hosts written for sendNativeMessage often exit after replying, which is correct for one-shot use and fatal for connectNative: the port receives one reply and then disconnects with “has exited”. Exit only on stdin EOF.

A five-minute diagnostic orderTime the disconnect, run the host by hand with a minimal environment, check executability, capture stderr from the browser launch, and hex-dump stdout.Time itms to disconnectRun by handenv -i, framed inputExecutable?x bit, arch, quarantineif it works by hand but not in the browserBrowser stderr--enable-loggingHex-dump stdoutfirst byte is {Lifetimeexit only on EOF
Stop at the first step that shows a problem — later steps assume the earlier ones pass.

Cross-browser variation

  • Chrome / Edge: the message is “Native host has exited.” Chrome kills the process when the port is disconnected, so a hang on the host side looks like a timeout in your extension rather than this error.
  • Firefox: port.error.message reads “An unexpected error occurred” or similar for several of these cases; check the Browser Console, which shows both the host’s stderr and Firefox’s own explanation of the launch failure.
  • Safari: no separate process — failures appear as errors from sendNativeMessage when the containing app’s handler throws or is missing. Check Console.app filtered by your app’s bundle id.

Verification

After the fix, run the diagnostic from step 1 again and expect a reply and no disconnect:

1host said { protocol: 3, version: "2.0.0", features: ["vault","watch"] }

Execution context: the service worker console. Leave the port open for a minute and confirm no disconnect appears; then call port.disconnect() and confirm the host process exits (check with ps aux | grep vault-host). Repeat on a clean user account or virtual machine — the original bug was very likely an environment difference, and your own machine is the one place it will not reproduce.

FAQ

The host works with sendNativeMessage but not connectNative. Why?

The host exits after replying. For one-shot use that is correct; for a port, the host must keep reading stdin until EOF.

It works in development but not for users. What usually differs?

The environment. Users’ machines lack your shell’s PATH, your installed runtimes, your Developer ID trust and your file permissions. Ship a single self-contained executable, sign it, and test on a fresh account.

Can the extension restart the host automatically?

Yes — reconnect in onDisconnect with exponential backoff and a cap, so a host that crashes on startup does not spin in a tight loop launching processes.

Other Core APIs & Cross-Browser Data Management Resources