# Fixing 1Password Classic form-filling on modern Firefox

*Authored by Claude 4.8 Opus.*

**Problem:** On Firefox 147 and later, the legacy **1Password Classic** extension
(the "1Password 4 / Mini" add-on, `onepassword4@agilebits.com`, used with the
1Password 7 desktop app) silently stops filling forms. The console shows:

```
TypeError: ... initKeyEvent is not a function
```

**Cause:** 1Password Classic simulates typing by calling the deprecated, non-standard
`KeyboardEvent.initKeyEvent()`. Firefox 147 disabled it and Firefox 149 **removed** it
([Bugzilla 2013772](https://bugzilla.mozilla.org/show_bug.cgi?id=2013772),
[2015079](https://bugzilla.mozilla.org/show_bug.cgi?id=2015079),
[1842988](https://bugzilla.mozilla.org/show_bug.cgi?id=1842988)). The extension is
unmaintained, so there is no upstream fix.

**This guide** patches the extension to use the standard `new KeyboardEvent()`
constructor instead — the same change Bitwarden made for the identical breakage — and
installs the patched copy so it keeps working on current Firefox. It is **durable**:
because the patched extension no longer calls the removed API, future Firefox versions
can't re-break it.

> ⚠️ **Security note:** 1Password Classic is unmaintained and has known unfixed issues.
> This keeps it *working*, not *secure*. The long-term-safe options are upgrading to a
> current 1Password app/extension or another maintained password manager. Use this if
> you have a deliberate reason to stay on Classic + the 1Password 7 desktop app.

---

## TL;DR

1. Find your installed `onepassword4@agilebits.com.xpi`.
2. Unzip it, change **one line** in `injected.min.js`, rezip it.
3. Install the patched copy on **Firefox Developer Edition** (release Firefox refuses
   modified/unsigned extensions) with `xpinstall.signatures.required = false`.
4. Keep the **original extension ID** so the 1Password desktop app still connects.

---

## Why the "easy" fixes don't work (so you don't waste time)

- **`about:config` → `dom.keyboardevent.init_key_event.enabled_in_addons = true`**
  Works only on Firefox 147–148. On 149+ the method is *gone*, not just disabled.
- **A separate "polyfill" extension** that re-adds `initKeyEvent`.
  Impossible: each Firefox extension runs in its **own isolated content-script sandbox**.
  1Password calls the API inside *its* sandbox; your extension can't reach it. The shim
  must live inside 1Password's own code — i.e. you must patch the extension itself.
- **Self-signing the patched extension via addons.mozilla.org so it runs on release
  Firefox.** AMO won't sign under AgileBits' add-on ID, so you'd have to change the ID.
  But the **1Password desktop app's helper actively reverts** its native-messaging
  allow-list to the original ID every time it runs, so a renamed extension can't keep
  talking to the app. Keep the original ID and use a Firefox build that allows unsigned
  extensions (below).

---

## Prerequisites

- The 1Password Classic extension currently (or previously) installed in Firefox.
- The **1Password 7 desktop app** (this extension is just a bridge to it).
- Ability to unzip/rezip a file and edit text. `python3` makes it easy (script below),
  but any zip tool + text editor works.

Profile and native-messaging locations by OS:

| | Firefox profiles | Native-messaging hosts |
|---|---|---|
| **macOS** | `~/Library/Application Support/Firefox/Profiles/` | `~/Library/Application Support/Mozilla/NativeMessagingHosts/` |
| **Windows** | `%APPDATA%\Mozilla\Firefox\Profiles\` | (registry / app-managed) |
| **Linux** | `~/.mozilla/firefox/` | `~/.mozilla/native-messaging-hosts/` |

The extension file is `extensions/onepassword4@agilebits.com.xpi` inside your profile.

---

## Step 1 — Get the extension file

Copy your installed xpi somewhere to work on it (don't edit it in place):

```sh
# macOS example — adjust the profile folder name (yours ends in .default-release)
PROFILE=~/Library/"Application Support"/Firefox/Profiles/*.default-release
mkdir -p ~/op-fix && cp "$PROFILE"/extensions/onepassword4@agilebits.com.xpi ~/op-fix/op.xpi
```

## Step 2 — Apply the one-line patch

Inside `injected.min.js` there is a helper that builds a keyboard event with the
removed API. It looks like this (variable letters may differ slightly by version):

```js
// BEFORE — uses the removed initKeyEvent
function H(a){var b;b=document.createEvent('KeyboardEvent');b.initKeyEvent(a,!0,!1,null,!1,!1,!1,!1,0,0);return b}
```

Replace it with the standard constructor (behaviour-identical: bubbles=true,
cancelable=false, all other args default):

```js
// AFTER — standard, works on every Firefox
function H(a){return new KeyboardEvent(a,{bubbles:!0,cancelable:!1})}
```

### Do it with a script (recommended)

Save as `patch.py` next to your `op.xpi`, then run `python3 patch.py op.xpi`:

```python
import re, shutil, sys, zipfile
from pathlib import Path

src = Path(sys.argv[1])
work = src.with_suffix(".unpacked")
out  = src.with_name("onepassword4-patched.xpi")
if work.exists(): shutil.rmtree(work)
with zipfile.ZipFile(src) as z: z.extractall(work)

inj = work / "injected.min.js"
text = inj.read_text(encoding="utf-8")
pat = re.compile(
    r"function (\w+)\((\w+)\)\{var \w+;\w+=document\.createEvent\('KeyboardEvent'\);"
    r"\w+\.initKeyEvent\(\2,![01],![01],null,![01],![01],![01],![01],0,0\);return \w+\}")
new, n = pat.subn(lambda m: f"function {m.group(1)}({m.group(2)})"
                            f"{{return new KeyboardEvent({m.group(2)},{{bubbles:!0,cancelable:!1}})}}", text)
if n != 1 or "initKeyEvent" in new:
    sys.exit(f"Expected exactly 1 call site, found {n}. Inspect injected.min.js by hand.")
inj.write_text(new, encoding="utf-8")

# CRITICAL: kill auto-update. The manifest's update_url points at AgileBits'
# server, which still serves the ORIGINAL (broken) extension. Left in place,
# Firefox periodically prompts to re-install it over your patch and re-breaks
# autofill. Remove update_url and bump the version so nothing can replace it.
import json
mp = work / "manifest.json"
man = json.loads(mp.read_text(encoding="utf-8"))
man.get("applications", {}).get("gecko", {}).pop("update_url", None)
man["version"] = man.get("version", "4.7.5.90") + ".1"
mp.write_text(json.dumps(man), encoding="utf-8")

# The original Mozilla signature no longer matches — remove it.
shutil.rmtree(work / "META-INF", ignore_errors=True)

# Repack. IMPORTANT: keep the original add-on ID (do not touch the manifest's id/key).
if out.exists(): out.unlink()
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
    for f in sorted(work.rglob("*")):
        if f.is_file(): z.write(f, f.relative_to(work).as_posix())
print("wrote", out)
```

### Or do it by hand

1. Rename `op.xpi` to `op.zip`, extract it.
2. Open `injected.min.js`, find `initKeyEvent`, make the one-line replacement above.
3. In `manifest.json`, **delete the `update_url` line** (under
   `applications.gecko`) and **bump the `version`** (e.g. `4.7.5.90` → `4.7.5.90.1`).
   This stops Firefox from re-installing the original broken extension over your
   patch — see the auto-update note below. Keep the **`id`** unchanged.
4. Delete the `META-INF` folder (its signature is now invalid).
5. Re-zip the **contents** (not a parent folder) and name it `onepassword4-patched.xpi`.

> **Why the `update_url` removal matters.** The extension manifest tells Firefox to
> auto-update from AgileBits' server, which still serves the **original**,
> `initKeyEvent`-broken extension. If you leave `update_url` in, Firefox will
> periodically pop up "do you want to install?" and, if applied, silently replace your
> patch with the broken version (blank toolbar icon, autofill dead again). Removing
> `update_url` and bumping the version prevents this permanently.

> Keeping the original ID is essential: the 1Password desktop app only authorizes that
> exact ID to connect, and it resets that list on its own. A renamed extension won't be
> able to talk to the app.

## Step 3 — Install on a Firefox that allows unsigned extensions

Release/Beta Firefox refuse modified extensions. **Firefox Developer Edition**,
Nightly, ESR, and Unbranded builds let you turn signature enforcement off. Developer
Edition is the simplest and stays current:

1. Install **[Firefox Developer Edition](https://www.firefox.com/en-US/channel/desktop/developer/)**
   (separate app and profile — your existing Firefox is untouched).
2. Launch it, open `about:config`, set **`xpinstall.signatures.required`** to **`false`**.
   Also set **`extensions.update.enabled`** to **`false`** as a safety net against
   auto-update. To make both permanent across pref resets, add a file named `user.js`
   in the Dev Edition profile folder containing:

   ```js
   user_pref("xpinstall.signatures.required", false);
   user_pref("extensions.update.enabled", false);
   user_pref("extensions.update.autoUpdateDefault", false);
   ```
3. Open `about:addons` → gear icon → **Install Add-on From File…** → choose
   `onepassword4-patched.xpi`.
4. Pin it: click the **puzzle-piece** (Extensions) toolbar icon → pin 1Password.

## Step 4 — Verify

- Click the 1Password toolbar button, or press your fill shortcut (e.g. **Cmd/Ctrl-\\**).
- It should open the 1Password Mini and fill username/password fields.
- If you want to watch for errors: `about:debugging` → This Firefox → the extension →
  **Inspect** → Console. A healthy run shows no `initKeyEvent` error and no repeated
  `No such native application` disconnects.

---

## Troubleshooting

- **"could not be installed because it could not be verified."**
  You're on release/Beta Firefox, or signature enforcement is still on. Use Developer
  Edition (or Nightly/ESR/Unbranded) and set `xpinstall.signatures.required = false`.
- **Toolbar icon is greyed out / `No such native application ...` repeats in the console.**
  The desktop app isn't connecting. Make sure you kept the **original extension ID**
  (don't rename it / don't self-sign under a new ID). Confirm the 1Password 7 app is
  running. The native-host manifest (`2bua8c4s2c.com.agilebits.1password.json`) should
  list `onepassword4@agilebits.com` in `allowed_extensions` — the app maintains this
  itself; don't fight it.
- **Fills the field but the website's form doesn't react.**
  The constructor patch dispatches standard events; this should behave like the
  original. If a specific site misbehaves, file it as a separate issue.
- **It worked, then broke again: blank toolbar icon, a "do you want to install?"
  popup, autofill dead but Cmd-\\ search still works.**
  Firefox auto-updated the extension from AgileBits' server back to the original broken
  version. Fix: rebuild the patch with **`update_url` removed and the version bumped**
  (Step 2), set **`extensions.update.enabled = false`** (Step 3), then remove and
  reinstall the extension. Click **Cancel** on any "do you want to install?" prompt —
  that's the upstream broken build. Confirm `about:addons` shows your bumped version
  (e.g. `4.7.5.90.1`), not the upstream `4.7.5.90`.
- **1Password updates the extension later.**
  Re-apply the patch to the new xpi. The script aborts loudly if the call site changed,
  so you'll know to re-inspect rather than ship a broken build.

---

## Optional — move your bookmarks/history/data to Dev Edition (no cloud)

Developer Edition uses a fresh profile. To bring your data locally (no Firefox Sync):

1. **Quit both** Firefox and Firefox Developer Edition completely.
2. Bookmarks **and** history live in `places.sqlite`; icons in `favicons.sqlite`.
   Copy them (and optionally `cookies.sqlite`, `logins.json` + `key4.db`,
   `formhistory.sqlite`, `permissions.sqlite`, `search.json.mozlz4`) from your old
   profile into the Dev Edition profile (`*.dev-edition-default`).
3. **Do not** copy the `extensions/` folder or extension state — reinstall add-ons from
   AMO instead, so the patched 1Password setup stays intact.
4. **Do not** point both Firefox builds at the same profile — that can corrupt it.

Back up the destination first so it's reversible.

---

## Using an AI assistant (Claude Code, etc.) to do this for you

If you'd rather have an LLM agent perform the steps, paste it something like:

> I'm on Firefox (version ___, macOS/Windows/Linux). The 1Password Classic extension
> (`onepassword4@agilebits.com`, used with the 1Password 7 desktop app) stopped filling
> forms with `initKeyEvent is not a function`. Please:
> 1. Find my installed `onepassword4@agilebits.com.xpi` in my Firefox profile and copy it
>    to a work folder.
> 2. In `injected.min.js`, replace the `document.createEvent('KeyboardEvent') +
>    initKeyEvent(...)` helper with the standard `new KeyboardEvent(type,{bubbles:true,
>    cancelable:false})`. Keep the original add-on ID. In `manifest.json` remove the
>    `update_url` and bump the `version` (so Firefox can't auto-update back to the
>    broken upstream build). Remove the now-invalid `META-INF` signature and repack as
>    `onepassword4-patched.xpi`.
> 3. Walk me through installing it on Firefox Developer Edition with
>    `xpinstall.signatures.required=false`, and verify filling works with the desktop app.
>
> Don't rename the extension ID or self-sign it — the desktop app only authorizes the
> original ID and resets its allow-list itself.

The agent should be able to locate the file, make the single-line change, and guide the
Developer Edition install. Review its changes before installing.
