# Your extension installs fine and never asks for the daemon

> A browser extension that installs cleanly, retries a localhost socket forever, and never says the daemon is a separate download. Plus a five-minute check.

- Author: Chad Priest
- Published: 2026-09-12
- Canonical URL: https://blog.vodou.ai/your-extension-installs-fine-and-never-asks-for-the-daemon/
- Tags: chrome, extensions, onboarding, debugging

---

On 2026-09-11 I wrote a note about my own product that I did not enjoy writing: installing the Vodou Bridge extension does not prompt anyone to install Vodou. On install it boots its content scripts and opens a socket to localhost. That is the entire first run. The thing on the other end of that socket is a separate download, and the extension never says so.

## The install succeeds, the socket retries, and nobody says a word

There is no symptom. Nothing turns red. The only artifact anywhere is a line in a service worker console that nobody opens:

```
WebSocket connection to 'ws://127.0.0.1:.../' failed:
Error in connection establishment: net::ERR_CONNECTION_REFUSED
```

That is a network event, not a thrown exception, so it lands in `onerror` and `onclose` and goes straight into the backoff. The shape, from `extension/Store-vodou-bridge/`:

```js
chrome.runtime.onInstalled.addListener(() => {
  bootContentScripts();
  connect();                       // no gate, no first-run check
});

function connect() {
  const ws = new WebSocket(LOCAL_WS);
  ws.onopen  = () => { backoff = 1000; markConnected(); };
  ws.onerror = () => scheduleReconnect();   // silent
  ws.onclose = () => scheduleReconnect();   // silent
}
```

`markConnected()` exists. There is no `markNeverConnected()`. The retry path cannot tell "the daemon is restarting" from "this user has never had a daemon in their life," because both arrive as the same refused connection.

## I spent months testing a build that no user gets

The reason I never saw it: my extension is the sideloaded build, and my daemon is always running. Those two facts each hide the bug on their own. The tree has three extension folders (`Store-vodou-bridge`, `vodou-bridge`, `sideload-only-vodou-bridge`) that all report the same version string, and the release packer `rm -rf`s two of them. The sideloaded build also gets a different extension ID than the Store build. So the artifact I clicked around in every day was not the artifact a stranger installs, and the state I needed to observe (no companion process, ever) has not existed on this machine since roughly the first week of the project.

The funnel is real, by the way. It is just in the side panel. We retired the popup on 2026-07-30 so the toolbar icon opens the panel directly, which is good ergonomics for a user who already knows what the panel is and useless for one who does not. Chrome will happily open an onboarding tab on `onInstalled`. We never set one.

## The class: a client that reads "never installed" as "temporarily down"

Any split client/companion architecture can install cleanly, fail to connect in the background, and never surface what is missing. MV3 extensions with a native messaging host. VS Code extensions that shell out to a CLI or a language server. Electron apps that assume a local model runtime like Ollama is already listening. MCP clients pointed at a local server. In every one of them the client's happy path is "connect," the unhappy path is "retry," and "the user does not have the other half" is not a path at all.

The best existing advice is permission-shaped. [steipete's chrome-enterprise notes](https://github.com/steipete/summarize/blob/main/docs/chrome-enterprise.md) put `nativeMessaging` in `optional_permissions` rather than `permissions`, so Chrome only asks after the user picks a Daemon runtime or clicks **Enable local companion**. That is genuinely good: the permission prompt becomes the onboarding moment, and it is enforced by the browser rather than by your code. It does not cover our case. A plain WebSocket to `127.0.0.1` requires no permission at all, so there is no prompt to hang the moment on, and the connect attempt fires from `onInstalled` before the user has clicked anything. [AutoCLI issue #15](https://github.com/nashsu/AutoCLI/issues/15) is the same hole one layer down: extension enabled, token saved, nothing listening on `localhost:19825`, and the truth is only available if you already knew to run `doctor` and read `✗ Daemon running`. A doctor command helps the user who suspects something. Ours had no reason to suspect anything.

**A client whose companion may not exist must resolve every failed connection into one of two named, persisted states, never-connected or was-connected-now-down, and the never-connected state must reach a surface the user did not have to navigate to.**

## Run this against your own client in a profile you have never used

Two checks, five minutes, nothing of ours involved.

```bash
# 1. Make the companion genuinely absent, not just stopped.
lsof -nP -iTCP:7777 -sTCP:LISTEN     # expect: no output

# 2. Install your client the way a stranger does: a blank profile.
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
  --user-data-dir=/tmp/vanilla-$(date +%s) --load-extension=./dist &
```

Then set a timer and touch nothing. Passing looks like something arriving unbidden inside those five minutes: a badge, a notification, an onboarding tab, an error item in the VS Code notification area. Failing looks like a clean console and a backoff loop.

The second check is the one that finds the real bug. Open your client's own storage and look for the distinction:

```js
chrome.storage.local.get(null, console.log)
// pass: { everConnected: false, installedAt: 1757... }
// fail: { token: "...", lastConfig: {...} }   // no field that means "never"
```

If nothing in there can answer "has this install ever succeeded once," your retry loop is treating a missing product as a flaky one, and it will keep doing that quietly for as long as the user leaves it installed. Grep your reconnect path for empty `catch {}` blocks while you are in there.

The rule I would put in any client that talks to a local companion: the first connection failure after install is a different event from the thousandth, and if your code cannot tell them apart, neither can your user.

---

Source: [Your extension installs fine and never asks for the daemon](https://blog.vodou.ai/your-extension-installs-fine-and-never-asks-for-the-daemon/) by Chad Priest, from Building Vodou in Public.
