# Installing Web Butler

Machine-followable instructions for installing the Web Butler Chrome
extension from source. Written for AI agents (Claude Code, Codex, Cursor,
and friends), but humans are welcome.

Web Butler is an open-source Chrome extension that puts the user's existing
AI subscription (ChatGPT, Claude, or Grok) to work on every page: answers
from page context, errands run in the browser, persistent page alterations,
and full reports. Repo: https://github.com/theswerd/web-butler

The simplest install is the Chrome Web Store listing, which handles
updates automatically:
https://chromewebstore.google.com/detail/web-butler/pbckigbcfinblmolfgiclbjchgdmhhpo
If the user asked you to follow this guide anyway, or wants a source
build, continue below.

## Ground rules for agents

- Follow these steps in order. Do not invent flags, paths, or settings.
- Do the whole install yourself, end to end, using Route A. Do not
  present the user a menu of options or ask which route they prefer.
  The standard Web Butler install prompt already grants permission to
  quit and relaunch Chrome (Chrome restores their tabs); only ask if the
  user's message explicitly withheld it.
- If a step fails, fall back to telling the user the exact remaining
  clicks rather than improvising.
- The extension talks to the hosted server at https://butler.swerdlow.dev
  when built with the production flag below. No API keys, database, or
  local server are needed for a normal install.

## 1. Build the extension

Requirements: git, Node 20+, npm.

```bash
git clone https://github.com/theswerd/web-butler
cd web-butler
npm install
WXT_SERVER_URL=https://butler.swerdlow.dev npm run build -w @web-butler/extension
```

(`npm run build:prod -w @web-butler/extension` is the same thing; the
spelled-out form works on any checkout.)

The unpacked extension lands in `apps/extension/.output/chrome-mv3`.
Note the absolute path; you need it below.

## 2. Load it into the browser

Pick one route.

### Route A: programmatic, the default (branded Chrome 126+)

Google removed the `--load-extension` flag from branded Chrome in 137.
The supported replacement is the CDP `Extensions.loadUnpacked` command,
which only works when Chrome is launched with BOTH of these flags:

- `--remote-debugging-pipe` (the command is not available over
  `--remote-debugging-port` / WebSocket)
- `--enable-unsafe-extension-debugging`

Chrome must be fully quit first; if a Chrome process is already running,
a new launch just forwards to it and the flags are ignored.

#### A.0: pick the right profile

Extensions install per Chrome profile. If the user has more than one
profile and you install into the wrong one, everything below "succeeds"
and yet the user sees nothing. Before launching, read Chrome's
`Local State` file (plain JSON, safe to read, never write to it) to list
profiles and find the one the user actually uses:

```bash
node -e '
const home = process.env.HOME;
const p = process.platform === "darwin"
  ? home + "/Library/Application Support/Google/Chrome/Local State"
  : home + "/.config/google-chrome/Local State";
const s = JSON.parse(require("fs").readFileSync(p, "utf8"));
for (const [dir, m] of Object.entries(s.profile.info_cache))
  console.log(dir + "\t" + m.name);
console.log("last used:\t" + (s.profile.last_used || "Default"));
'
```

The left column ("Default", "Profile 1", ...) is the directory name the
flag below needs; the right column is the human name shown in Chrome's
profile picker. Install into the **last used** profile. If the output
shows several profiles with real-looking names, install into each of
them by running the recipe once per directory name; the extension id is
derived from the path, so it is identical in every profile.

#### A.1: launch and load

The pipe speaks JSON messages terminated by `\0` on file descriptors
3 (in) and 4 (out). Complete recipe (macOS and Linux):

```bash
EXT="/absolute/path/to/web-butler/apps/extension/.output/chrome-mv3"
PROFILE="Default"   # directory name from step A.0, not the display name

# Quit any running Chrome cleanly; it restores tabs on relaunch.
osascript -e 'quit app "Google Chrome"' 2>/dev/null   # Linux: pkill -x chrome
sleep 2

node <<EOF
const { spawn } = require('node:child_process');
const chrome = process.platform === 'darwin'
  ? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
  : 'google-chrome';
const child = spawn(
  chrome,
  [
    '--remote-debugging-pipe',
    '--enable-unsafe-extension-debugging',
    '--profile-directory=${PROFILE}',
  ],
  { stdio: ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'] },
);
child.stdio[3].write(JSON.stringify({
  id: 1,
  method: 'Extensions.loadUnpacked',
  params: { path: '${EXT}' },
}) + '\0');
let buf = '';
child.stdio[4].on('data', (d) => {
  buf += d;
  const end = buf.indexOf('\0');
  if (end === -1) return;
  const msg = JSON.parse(buf.slice(0, end));
  console.log('loadUnpacked response: ' + JSON.stringify(msg));
  // Give Chrome a moment to flush the install to disk, then let it
  // close (Chrome shuts down when the CDP pipe closes with us).
  setTimeout(() => process.exit(msg.result && msg.result.id ? 0 : 1), 1500);
});
EOF
```

A success response is `{"id":1,"result":{"id":"<extension-id>"}}`.
Record the extension id for step 3. When the script exits, Chrome closes
with it; relaunch Chrome normally (no flags) and the extension persists
like any developer-mode unpacked extension. If the response is an error,
or the extension is missing after a normal relaunch, fall back to
Route B; do not improvise other mechanisms.

Notes:

- The `--profile-directory` flag pins the install to a specific profile
  of the user's real browsing data, not a throwaway session. Repeat the
  recipe (quit Chrome, relaunch with the next directory name) for every
  profile the user works in.
- Windows: the pipe uses POSIX file descriptors; run the recipe under
  WSL, or use Route B. (Chrome's data lives at
  `%LOCALAPPDATA%\Google\Chrome\User Data`; `Local State` sits at its
  root, same format.)
- Chromium, Chrome for Testing, and most non-Google Chromium browsers
  (e.g. Vivaldi) still support the old `--load-extension="$EXT"` flag,
  but it is session-scoped: it does not persist. Prefer this route or
  Route B even on those browsers.

### Route B: by hand (fallback only)

1. Switch to the profile the user actually browses in (avatar button,
   top right of Chrome). Loading unpacked only affects the current
   profile.
2. Open `chrome://extensions`.
3. Turn on "Developer mode" (top right).
4. Click "Load unpacked" and select
   `apps/extension/.output/chrome-mv3`.

This persists across restarts like any developer-mode extension. Repeat
in each additional profile the user works in.

### Dead ends: do not attempt these

Agents keep inventing these; none of them work on current Chrome.

- Do not copy files into the profile's `Extensions/` directory
  (`.../Default/Extensions/<id>/<version>/`). That store is managed by
  Chrome for store-installed extensions; manually placed files are
  ignored.
- Do not edit `Preferences` or `Secure Preferences` to register the
  extension, enable it, or flip its settings. Extension entries are
  HMAC-signed; Chrome detects tampering and resets or disables the
  entry.
- Do not pass `--load-extension` to branded Google Chrome: it has been
  ignored since Chrome 137. Even where it works it is session-only.
- Do not launch with a temporary `--user-data-dir`: the extension would
  install into a throwaway profile instead of the user's real one.
- Do not "copy the install" into other profiles by duplicating files
  under `Profile N/Extensions/` or editing their `Preferences`. Same
  HMAC problem as above. To cover another profile, rerun Route A with
  that profile's `--profile-directory`.

## 3. Enable "Allow User Scripts"

Web Butler's persistent page alterations use Chrome's `userScripts` API,
which is gated behind a per-extension toggle (Chrome 138+): open
`chrome://extensions/?id=<extension-id>` and turn on **Allow User
Scripts**. Without it the extension still works, but installed page
alterations will not run. The toggle is per profile: flip it in every
profile you installed into.

Do this yourself too. `chrome://extensions` is a normal page; launch
Chrome with `--remote-debugging-port=9222 --profile-directory=<dir>`
(same directory name as the install), attach to the
`chrome://extensions/?id=<id>` target, and click the toggle through the
shadow DOM:

```js
const view = document.querySelector('extensions-manager').shadowRoot
  .querySelector('extensions-detail-view').shadowRoot;
const row = [...view.querySelectorAll('extensions-toggle-row')]
  .find((r) => /user scripts/i.test(r.textContent));
row.shadowRoot.querySelector('cr-toggle').click();
```

If the selector chain fails (Chrome WebUI changes between versions), hand
the toggle to the user rather than retrying blindly: the extension itself
detects the missing permission and deep-links to the exact setting.

Note the flag difference: this step uses the ordinary debug port, not the
pipe. Relaunching Chrome again for it is fine; you already have permission
for restarts.

## 4. Verify, then hand off for sign-in

Success criteria, in order. Check each one and report the result. All
checks happen **in the profile the user actually uses** (and in each
profile, if you installed into several); a pass in the wrong profile
proves nothing:

1. `chrome://extensions` lists "Web Butler", enabled, with no error
   badge, after a normal (flag-free) Chrome relaunch.
2. The Allow User Scripts toggle on its detail page is on.
3. On any normal webpage (not a chrome:// page), `Cmd+E` (`Ctrl+E` on
   Windows/Linux) opens the Web Butler prompt bar.
4. Onboarding asks the user to connect ChatGPT, Claude, or Grok. This is
   a real sign-in on the provider's site; only the user can do it. Stop
   here and hand off.
5. After connecting, asking something about the page returns an answer.

## FAQ for agents

**The install "succeeded" but the user sees no extension?** Almost
always a profile mismatch: the extension went into one Chrome profile
and the user browses in another. Rerun step A.0 to list profiles, then
rerun the Route A recipe with the right `--profile-directory`. Also
confirm Chrome was fully quit before the flagged launch; if a Chrome
process was already running, the flags were silently ignored.

**Can this be installed with no browser process at all?** Not on consumer
Chrome, by design. The CLI flag was removed in Chrome 137, Chrome's
`Secure Preferences` are HMAC-protected against direct edits, and
policy-based force-install (`ExtensionInstallForcelist`) only applies to
enterprise-managed machines. Route A is the sanctioned near-headless
path: you launch the browser once yourself; the user never has to.

**Does the user need their own server?** No. `build:prod` points the
extension at the hosted server. To self-host instead, follow the README
in the repo (Postgres + Freestyle API key), build with
`WXT_SERVER_URL=<your-server>`.

**Firefox?** There is a `build:firefox` script, but Chrome is the
supported target today.

Questions or breakage: https://github.com/theswerd/web-butler/issues
