Developing Zotero plugins with AI
You use Zotero daily, you are missing a feature, and modern AI coding agents can write most of the code — but a Zotero plugin is a peculiar thing (it runs inside a Firefox-based app, with its own APIs, lifecycle, and community rules), and an agent left alone will guess wrong about all of it. This page serves two purposes:
- Part 1 — Starting from scratch: the path from nothing to a working, publishable plugin, with the ecosystem’s tooling, rules, and a resource directory. No deep programming experience required — the agent is the hands; you are the judgment.
- Part 2 — The Weavero workflow in depth: the concrete day-to-day setup used to develop Weavero — giving the agent direct access to a live, running Zotero, scripted in-app benchmarks, and a gotchas index where every entry cost real time once. Nothing in it is theoretical.
The shortcut: give this page to your AI. This page is written to be read by AI agents as much as by humans. Paste its URL (or its content) into your AI coding agent together with your plugin idea, and the agent will guide you through the steps below to build the plugin — explaining each one, doing the coding and in-app verification it can do itself, and walking you through the parts that need your hands (installing Zotero, creating the sandbox profile, installing the bridge, testing real gestures). You stay the judgment: read what it writes, test what it ships.
The experience behind this page is with Claude Code in VS Code; the pieces are MCP-standard, so any MCP-capable agent should work.
Part 1: Starting from scratch
What you need
- Zotero — ideally the beta for development (recent APIs, and betas don’t enforce plugin compatibility ceilings while you iterate).
- An AI coding agent (Claude Code or any MCP-capable equivalent).
- Node.js (≥ 20), git, and an editor (VS Code works well with agent integrations).
- The willingness to read what the agent writes and to test everything yourself.
Step 1 — Isolate: a dedicated profile and data directory
Before anything else — and this applies to any Zotero development,
with or without an AI agent — separate your development environment
from your real library. Create a dedicated Zotero profile with its own
data directory, following the official guide:
Multiple Zotero profiles
(zotero.exe -P opens the profile manager; give the dev profile its
own data directory in Settings → Advanced → Files and Folders). Sync it
to a throwaway account or not at all.
Everything development does — installing work-in-progress builds, executing scripts, generating test data, letting newer builds upgrade the database schema (a one-way operation) — happens in that profile. An agent that can execute code in Zotero can also corrupt a database; the sandbox makes that a non-event. The agent gets access to the dev profile only, never the one holding your real library.
Isolation, level two — a source-built Zotero. Once you are past the basics, a source-built Zotero (see the official build docs) on a third profile lets you test your plugin against upstream HEAD — weeks before changes reach a beta. It is a different binary, so it runs alongside your daily Zotero; give each instance its own bridge port (Part 2 shows how). Weavero catches upstream collisions (row-model changes, search reworks, docShell behavior changes) this way, on the day they land — and the one-way schema rule above is exactly why this sandbox, too, gets its own data directory.
Two low-friction on-ramps before you build anything. You can learn most of what matters about Zotero’s API without a plugin skeleton:
- Tools → Developer → Run JavaScript — a built-in console with full
privileged access to the running Zotero (tick Run as async function
for
await). Probe what the API really returns (Zotero.Items.get(…),Zotero.getMainWindow()…), paste snippets the agent proposes and read the result, and run maintenance or test scripts — Weavero’s manual test protocols are plain scripts pasted here. The official JavaScript API guide documents this console and the core API patterns. - Actions & Tags — a plugin that runs small user scripts on events (item added, tab opened…) or from menus/shortcuts. It is the gentlest way to automate basic operations and grow real API experience with zero build tooling — and a legitimate destination in itself if all you need is an automation. Weavero itself began life as an Actions & Tags action script and only later became a standalone plugin; when a script outgrows the harness, Step 2 is waiting.
Step 2 — Start from the template, not from a blank folder
The community maintains a modern plugin stack; starting there saves the agent from reinventing (badly) what already exists:
- zotero-plugin-template — a complete starter plugin: TypeScript, build config, CI, hot reload. Click “Use this template” and you have a building, installable plugin before writing a line.
- zotero-plugin-scaffold — the build/test/release tool the template uses (and Weavero uses standalone): bundles the XPI, launches a test Zotero, publishes releases.
- zotero-plugin-toolkit — helper APIs for common plugin needs (UI, prefs, shortcuts).
- zotero-types —
TypeScript definitions for Zotero’s APIs. These matter more with an
AI agent: a strict type check (
tsc --noEmit) is a mechanical gate that catches a whole class of agent mistakes before runtime. - make-it-red — Zotero’s own minimal sample plugin, useful as a reference for the bare lifecycle.
Step 3 — Frame the agent: ground truth and standing rules
This step matters more than any amount of prompting skill. An AI agent without framing goes in blind: it answers from training data that predates current Zotero, searches the open web where plugin information is sparse, outdated, or plain wrong, and — when it finds nothing — it can hallucinate an API that sounds plausible and does not exist. Framing the agent fixes this at the root, three pieces:
- Give it access to the real documentation and tools — locally or through links. Clone the Zotero source (plus reader and note-editor if you touch those areas) somewhere the agent can search, and point it at the documentation list below. A local clone beats a web search every time: the agent greps the exact version you run.
- Dictate standing rules it always remembers — instruction files
(Claude Code reads
CLAUDE.mdfrom the repo root; other agents have equivalents). Record how to build, how to test, the project’s conventions, and every invariant you learn the hard way — this is the context in which the agent takes decisions, and it is reloaded every session, so no lesson has to be learned twice. - Make evidence-before-answers a standing rule. Require the agent
to base every claim on real data — read the source code or the
documentation (or probe the live app, Step 4) before replying, and
state which sources it used. An answer that cites
chrome/content/zotero/tabs.jsor a documentation page is checkable; an answer from memory is a guess wearing a confident tone. On Weavero this rule is written down as “verify, don’t guess” — Part 2 shows how it is enforced in practice, and the permissions side of the framing completes it.
The agent-side fundamentals have an excellent official tutorial:
Best practices for Claude Code
covers writing an effective CLAUDE.md (what to include, what to
prune), configuring permissions, giving the agent checks it can run,
and managing context — the principles transfer to any coding agent.
This page stays focused on the Zotero-specific side.
Documentation to point the agent (and yourself) at:
- Zotero Plugin Development docs — the community handbook (windingwind): environment setup, plugin anatomy, lifecycle, data model, privileged vs. unprivileged operations, real-world how-tos, and an API reference.
- The official per-version developer notes —
Zotero 7 for developers
(plugin architecture, main-window hooks, bootstrap lifecycle) and
Zotero 8 for developers
(JSM → ESM modules, Bluebird removed in favor of standard promises,
preference panes in isolated scopes, the
MenuManagerAPI). Read the page for every major version you target: the breaking-changes lists are precisely what AI agents misremember — an agent’s training data predates these changes, so left unguided it will write Zotero-7-era code (.jsmimports, Bluebird promise methods) that no longer exists. - Zotero 8 Plugin Development Guide
(community, EwoutH) — a deep single-page reference: lifecycle-hook
tables, the 7 → 8 migration (including the
migrate-fx140scripts), Fluent localization, custom menus/columns/item-pane APIs, reader event handlers, packaging. - The Zotero developer hub — the umbrella index: client coding, the web API, translators (a beginner-friendly side entry into the ecosystem), citation styles.
- Zotero source-code search — when the docs run out, the source is the documentation.
Ask for prior art. The best way to solve a problem is very often to look at what others have done in similar situations — and an AI agent is exceptionally good at that research if you ask for it explicitly:
- Other Zotero plugins — someone has probably faced your problem; open-source plugins are searchable answers. Have the agent study how they solved it before inventing an approach.
- Zotero itself — the cleanest pattern for almost anything is how Zotero’s own code does it (that is what the source mirror is for).
- The software that pioneered the behavior you want — when Weavero needed tab tear-off, drag-and-drop, and window-focus semantics, the answers came from reading Firefox’s own tabbrowser source, not from guessing; its test methodology borrows from Better BibTeX and Zotero’s suite. Whatever UX you are copying, its origin has already solved the edge cases.
- The sources on this page — tell the agent to search the docs, source mirrors, and guides listed here when it is stuck. Agents don’t reliably reach for references unprompted; “check how X does it” and “look through the linked docs first” are among the highest-value instructions you can give.
When code patterns are adapted from what you find, remember the licensing rule below: license compatibly and credit the origin.
Step 4 — Give the agent eyes and hands
The single biggest upgrade to AI-assisted plugin work is letting the agent interact with a live Zotero: execute JavaScript, install and reload builds, read the error console, query the database, take screenshots. That is what the MCP bridge for Zotero provides; the full setup (including running two Zotero instances) is in Part 2. Install the bridge in the dev profile from Step 1 only — the whole point of the sandbox is that the agent’s hands never reach your real library. And capable hands deserve configured limits: see parametrise the agent in Part 2.
Step 5 — Iterate in small, verified steps
The loop that works: describe one small behavior → the agent implements it → typecheck gate → build → install into the sandbox → verify in the running app (the agent can do much of this through the bridge; you test the parts that need real gestures) → commit. Resist big-bang features: agents are excellent at small verified increments and unreliable at thousand-line leaps. Zotero’s install/reload machinery has sharp edges (caching, stale handlers, version rules) — they are catalogued precisely in the edit–install–verify loop and the gotchas index below.
The ecosystem’s rules — read before publishing
- Never write to
zotero.sqlitedirectly. The Zotero developers’ position is unambiguous: plugins must go through the data APIs; plugins that modify the database “won’t be allowed into a future official plugin directory and could conceivably be banned from running altogether” (forum statement). If you need persistent storage, attach your own database or use your own data files. - Do not claim compatibility with an unreleased Zotero major
version. Test on the beta (compatibility is unenforced there), but
only bump
strict_max_versionafter the feature-freeze announcement for that release — Zotero reserves the right to block plugins that declare far-future compatibility (zotero-dev policy). - License compatibly and credit upstream. Zotero is AGPL-3.0; if your plugin adapts Zotero source (agents do this a lot — make them tell you when), license compatibly and credit the origin.
- Ask on zotero-dev — the developer mailing list is active and the Zotero team answers; the forums are the place for user-facing questions and feature discussions.
Publishing
Release through the scaffold (it builds the XPI, creates the GitHub
release, and maintains the update.json that gives your users
auto-updates). To be discoverable, get listed in the
zotero-addons-scraper
registry, which feeds the in-Zotero
Add-on Market plugin most
users browse. (An official Zotero plugin directory is
planned and expected to
replace the third-party lists — that official plugins page is the
likely place it will appear, so it is the one to watch. Meanwhile the
NGI0-funded plugin-ecosystem project
— a community effort, not affiliated with Zotero — is consolidating
the community tooling above at
zotero-plugin.dev, where a community
plugin registry is under development.)
Part 2: The Weavero workflow in depth
What makes AI-assisted development work well on Weavero is not only the code generation — it is that the agent acts on what it observed in the running app instead of what it guessed. Everything below was learned building Weavero against Zotero 7–10.
The bridge: let the agent drive a live Zotero
Two components connect the agent to Zotero:
- MCP Bridge for Zotero — a small Zotero plugin that opens a
Firefox Remote Debugging Protocol (RDP) server inside Zotero
(localhost only). Zotero is Firefox under the hood, so the whole
DevTools server ships with it; the plugin just opens the socket.
Download
zotero-mcp-bridge.xpifrom the mcp-server-zotero-dev releases and install it via Tools → Plugins → ⚙️ → Install Plugin From File (in the dev profile — see Step 1 of Part 1). @introfini/mcp-server-zotero-dev— an MCP server (run vianpxfrom the npm package) that translates agent tool calls into RDP requests:zotero_execute_js,zotero_plugin_install,zotero_plugin_reload,zotero_db_query,zotero_screenshot,zotero_read_errors, DOM inspection, prefs, and more. Both halves live in the same repository.
Register it with the agent (one-time):
claude mcp add -s user zotero-dev -- npx -y @introfini/mcp-server-zotero-dev
On Windows, run that in cmd, not PowerShell — PowerShell strips the
bare -- separator. And when passing environment variables, the server
name must come before the variadic -e flags:
claude mcp add -s user zotero-dev-source -e ZOTERO_RDP_PORT=6101 -- npx -y @introfini/mcp-server-zotero-dev
That second form matters as soon as you run two Zotero instances
(say, your installed beta and a built-from-source HEAD): give each its
own bridge port and register one MCP server per instance. The port pref
the bridge plugin reads is extensions.zotero.extensions.mcp-rdp.port
(per profile, not per binary). A brand-new MCP server needs a full
agent restart to appear — a mid-session reconnect is not enough.
Teach the project to the agent: instruction files
Three habits carried the most weight in Weavero’s instruction files:
- “Verify, don’t guess.” The agent must not assert how Zotero or Firefox behaves from memory — it greps the local source mirror or probes the live runtime through the bridge, then states the behavior. One wrong from-memory claim about saved-tab behavior cost a full debugging round-trip; this rule exists because that happened.
- Write down the invariants that were paid for with regressions,
next to the code and in the instructions (“open note tabs only via
ZoteroPane.openNote”, “CSSdata-item-typevalues are camelCase and kebab-case fails silently”, …). The agent reads them every session; the regression never repeats. - Post-edit integrity checks: TypeScript gate (
tsc --noEmit, zero errors, wired as aprebuildstep), XML well-formedness for prefs panes (named HTML entities like are not defined in XHTML and blank the pane — use ), JSON parse + version match for manifests. Cheap, mechanical, and they catch the classic agent-editing failure modes.
Weavero’s instruction files themselves are private working notes, but
their operative content is distilled in this page and in the public
testing contract: TESTING.md
(which test suite to run on which kind of change), the
bench/
performance protocol, and the manual test protocols for what automation
can’t reach (restart/session,
gestures, plugin-disable,
taskbar overlays).
Parametrise the agent: permissions and guardrails
Instruction files teach the agent what is true; the agent’s own
configuration decides what it may do — and tuning it is as important
as the prompts. Every serious agent has a permission system (in Claude
Code: settings.json with allow/deny rules per tool and command
pattern). Three settings carry most of the value:
- Deny the destructive commands outright. Weavero’s deny list
blocks force-pushes,
git reset --hard,git clean -f, forced branch deletion, process kills (Stop-Process— see the never-force-kill rule below), and the release command. A guardrail like this is not distrust of the agent — it converts “the agent made a destructive slip” into “the agent had to stop and ask”, which is exactly the failure mode you want. These rules have blocked real mistakes on this project, and a deliberate, human-confirmed exception remains possible. - Allowlist the routine loop. The build/typecheck/test commands and the bridge’s read-mostly tools should run without prompting — an iteration loop that asks permission forty times a session trains the human to click yes blindly, which is worse than either extreme.
- Scope the reachable directories. The agent gets the plugin repo, its working folders, and the sandbox — not your home directory.
Revisit the lists as habits form: promote commands that always get approved, demote anything that ever surprised you. For the Claude Code specifics (permission modes, allowlists, sandboxing, hooks), see the official best-practices guide; the examples throughout this page use Claude Code’s configuration, but every capable agent has equivalents (permission rules, project instruction files, tool allow/deny lists) — translate the idea, not the exact syntax.
The edit–install–verify loop
- Edit TypeScript →
npm run build(the scaffold bundles the XPI). zotero_plugin_installwith the built XPI path (bump the version every build — Zotero will not reinstall the same version id).- Force-reload with cache bypass — the bytecode cache happily
serves last build’s code after an install.
zotero_plugin_reload, orloadSubScriptWithOptions(rootURI + "bootstrap.js", {ignoreCache: true}). - Verify through the bridge: is the new behavior live? any new errors
in
zotero_read_errors?
Two lifecycle rules the loop depends on:
- Hot-reload leaves stale DOM handlers. After a reload, re-open the
affected tab — or restart Zotero — before trusting a test result. For
anything serious we cold-restart:
Zotero.Utilities.Internal.quit(true). - Never force-kill Zotero (
Stop-Process, task manager) as part of automation: unflushed state bites later. A force-kill once resurrected an add-onuserDisabledflag that had been toggled back, which then “mysteriously” disabled the plugin two restarts later.
Scripted in-app testing and benchmarks
The bridge turns tests and benchmarks into plain JavaScript executed
inside Zotero: generate fixtures (hundreds of annotations in one DB
transaction), drive the UI, sample requestAnimationFrame deltas for
frame-time statistics, read back structured JSON. Weavero’s benchmark
suite lives in
bench/ —
reader-load timings, sidebar/PDF scroll jank, window-machinery timings —
and it caught real regressions in our own “optimizations” the day they
were written. The patterns that matter:
- Keep each evaluated script under ~20 seconds. Long evals die with the bridge’s request timeout, especially when the main thread is busy with layout/rendering work. Structure benchmarks as one-run-per-invocation and loop invocations from outside.
- Cross the Xray membrane explicitly. Chrome-side arrays and objects
passed into reader-internal APIs throw
Permission denied to access property "length"— wrap them withComponents.utils.cloneInto(value, targetWindow). - Synthetic events have limits.
dispatchEventfiresaddEventListenerhandlers (fine for HTML UI), but XUL command handlers ignore untrusted events, and some flows (opening the in-PDF annotation popup) only respond to real user input. Test those manually and script everything else. - Anchor measurements on stable landmarks (a specific card index via
scrollIntoView), not onscrollHeightfractions — layout height changes with what you are testing, so fractional anchors quietly measure different content across configurations. - Beware shells eating backslashes. Generating LaTeX test fixtures
through a shell command corrupted every
\r,\n,\tmacro into control characters. Write scripts to a file and have the bridge read the file.
Gotchas index
The compressed list — each of these cost real time once:
| Gotcha | Rule |
|---|---|
PowerShell strips -- |
register MCP servers from cmd |
| Bytecode cache after install | force-reload with ignoreCache |
| Same-version reinstall is a no-op | bump the version every build |
| Hot-reload keeps stale DOM handlers | re-open tabs / restart before testing |
| Force-kill loses add-on state | always quit gracefully |
| Xray membrane | cloneInto everything passed into reader internals |
| Untrusted events | XUL command handlers ignore synthetic clicks |
| XHTML prefs panes | numeric character references only ( , not ) |
data-item-type CSS |
camelCase (attachmentPDF); kebab-case fails silently |
| Bridge eval timeout | keep scripts < ~20 s, loop from outside |
| Closures go stale across plugin reloads | resolve the live plugin object at event time |
localStorage/sessionStorage |
unreliable in Zotero — use Zotero.Prefs or your own files |
| Agents write Zotero-7-era code | ground them in the per-version developer notes (ESM, standard promises) |
What this buys you — honestly
With this setup the agent routinely: diagnoses a startup crash by reading the live error console and bisecting with add-on disable/enable cycles; verifies a fix by driving the exact UI flow; measures a performance claim instead of asserting it; and checks compatibility against an upstream commit the day it lands. What it does not replace: real user gestures (drag-and-drop feel, popup interactions), taste, and the final judgment on what ships. The division of labor that works: the agent proposes and verifies against the live runtime; the human reviews diffs, tests the gestures, and decides.
Resource directory
| Category | Resource |
|---|---|
| Interactive console | Tools → Developer → Run JavaScript · JavaScript API guide |
| Script harness / automation | Actions & Tags |
| Template | zotero-plugin-template |
| Build/release tool | zotero-plugin-scaffold |
| Helper APIs | zotero-plugin-toolkit |
| TypeScript types | zotero-types |
| Minimal sample | make-it-red |
| Community handbook | doc-for-zotero-plugin-dev · Zotero 8 Plugin Development Guide |
| Official dev notes | dev hub · Zotero 7 · Zotero 8 for developers |
| Ground truth | zotero/zotero · reader · note-editor |
| AI ↔ live Zotero | MCP bridge + server |
| Agent-side tutorial | Best practices for Claude Code (CLAUDE.md, permissions, verification, context) |
| Community | zotero-dev list · forums |
| Distribution | addons-scraper registry · Add-on Market |
| Community ecosystem project | zotero-plugin.dev · NGI0 grant |
| Worked example | Weavero source |
This page reflects the Weavero workflow as of July 2026 (Zotero 10 beta). Corrections and additions welcome — open an issue.