A Zotero plugin that watches a folder, imports new files into the right collection, fetches metadata, renames them from a template, and keeps your collection tree mirrored to disk — both ways, with a recoverable trash and conflict gate so nothing gets shredded by accident.
You download a paper. You mean to add it to Zotero. You don't. Six months later
it's 1706.03762.pdf next to 1706.03762 (3).pdf, and
you've already forgotten what either one was.
_needs-review tag on failure).Modes are a single preference. Switch any time — the plugin starts and stops the relevant pipeline in-process, no Zotero restart required.
New in v2.7: whole-library scope (default).
By default the plugin now mirrors your entire Zotero library, not just
one collection. A PDF dropped at the watch-folder root becomes an Unfiled item;
every top-level collection becomes a top-level folder; nested collections map to
nested folders. Special views (Duplicates, Unfiled view, saved searches, Trash)
are never mirrored. Existing installs are automatically pinned to their current
collection-scope behavior on upgrade — whole-library scope is opt-in for them via
re-running setup. The scopeMode preference controls this: library
(new default) or collection (single chosen sync-root, legacy behavior).
.zotero-watch-trash/The plugin is intentionally boring: the interesting part is what it enables. Here's how three different setups use it.
Configures the watch folder to ~/Downloads/papers, sync root
Research/Thesis, Mode 2. Every PDF saved from a browser
lands in Zotero within seconds, properly named, in the right sub-collection.
Smart rule auto-tags anything with "transformer" in the title.
Already has a deep folder tree under ~/Library/PDFs from years
of manual organization. Runs first-run baseline in Mode 3 — the plugin
adopts every existing PDF into matching collections, mkdirs empty
collections, and from then on keeps them in lockstep.
Renaming a collection in Zotero renames the folder on disk; deleting
a folder on disk trashes the matching collection (with a confirm
dialog for anything > 10 files).
Points the watch folder at the shared Dropbox/OneDrive/Syncthing directory in Mode 1 (import-only — collaborators editing the folder shouldn't affect their Zotero library). New files her advisor drops appear in her library overnight, dedup'd against what she already has, tagged for review if metadata couldn't be retrieved.
Reality, not roadmap. If it's on this list, it works end-to-end against a live Zotero and is covered by the unit suite.
Polling-based with adaptive backoff. Recursive subfolder scan; subfolders become collection paths.
Hash-first dedup catches re-imports even when the local tracking store is wiped. Falls back to DOI / ISBN / fuzzy title after metadata.
Queued, throttled, polite. Failed lookups get a _needs-review tag so nothing slips through silently.
{firstCreator} - {year} - {title} and friends. Sanitized, length-capped, optional.
Match on title / author / DOI / tags / filename. Apply add-to-collection, add-tag, set-field, or skip-import. JSON editor opens in a separate window from the prefs pane's Advanced section.
Collection ↔ folder lockstep in Mode 2/3. Renames, moves, and new folders propagate both ways.
Mirror your entire Zotero library by default (scopeMode: library). A PDF dropped at the watch-folder root lands in Unfiled; every top-level collection becomes a top-level folder. Switch to collection scope for the legacy single-root mode. Library-scale Mode 3 adds cloud-eviction pause (if >50% of top-level folders vanish at once, deletion pauses) and a one-time enable confirmation before the first delete.
Mode 3 moves to .zotero-watch-trash/ (recoverable). Six-case restore matrix: file or folder, by hash or by attachment.
Any single op affecting >10 files or >20% of tracked items prompts confirmation. Refuses if the prompt can't render.
If a file's content has drifted, no move happens. The record flips to conflict-blocked and surfaces in the prefs pane.
Adopts an existing tree without re-importing: hash-based cross-path reconcile, mkdir for empty collections, idempotent per sync-root.
Last 100 warnings categorized (conflict, missing file, I/O, suppressed, unknown). Visible from the prefs pane.
Flip mode1 ↔ mode2 ↔ mode3 live. Coordinators start/stop in-process — no restart.
A Settings tool that scans for inconsistencies between your watch folder, Zotero, and the plugin's tracking. Each issue is shown with a plain-English explanation and a recommended fix; applies only what you approve. Items with annotations or notes are shown first and never deleted. Never deletes files and never bypasses Zotero — repairs are tracking fixes and additive collection membership only.
Most of the v2.2 work went into the destructive-path safety net. Here's what stands between a misclick and a lost paper.
Mode 3 trashes go to .zotero-watch-trash/<rel> under your watch root. Collision-suffixed (name.<ms>.ext) so nothing is overwritten. Skipped by the scanner so it isn't re-imported.
If a single operation would affect more than 10 files or 20% of your tracked items, the plugin shows a Services.prompt confirm. If the prompt can't render (no UI thread), it refuses rather than silently executing.
canSafelyMove hashes the live file against the last known stamp. Drift = no move; record flips to conflict-blocked; you resolve via the prefs UX (re-stamp / discard / pause sync).
If you keep two copies of the same file under the watch root, only the canonical copy is ever disk-deleted. Shadow records are dropped from tracking without any FS action — the Zotero attachment is never trashed by accident.
Every mutation goes through a single executor with promise-chain locks per attachment:<key> / collection:<key>. Concurrent notifier batches can't interleave through stale state.
Files can be restored four different ways: un-trash in Zotero, restore the local file by hash, re-attach to a parent whose attachment was purged, or restore an entire folder. All collision-handled.
No CLI required for end users. The .xpi install is a single click;
the rest is a guided preference pane.
.xpiGrab the latest release from GitHub Releases. Current build: v2.8.4.
Open Tools → Add-ons (Zotero 7) or Tools → Plugins (Zotero 8/9). Click the gear, choose Install Add-on From File…, pick the .xpi. Restart Zotero when prompted.
Open Edit → Settings → Watch Folder. Set Source Folder to where your PDFs land — ~/Downloads, a Dropbox folder, anything writable.
Choose the Zotero collection that anchors your sync. Pick mode1 for import-only, or step up to mode2 / mode3 once you trust the mirror. Toggle Enable Watch Folder. You're done.
Defaults are sane. These are the knobs you'd reach for first. Everything
lives under extensions.zotero.watchFolder.* and is accessible
from about:config if you need to script it.
| Preference | Default | What it does |
|---|---|---|
| enabled | false |
Master on/off switch. Toggles the scanner and sync coordinator in-process. |
| sourcePath | "" |
The folder being watched on disk. |
| mode | mode1 |
Sync mode: mode1 (import only), mode2 (mirror no delete), mode3 (mirror with safe delete). |
| syncRootCollectionKey | "" |
The 8-char Zotero collection key that anchors the sync. Only relevant when scopeMode is collection. |
| scopeMode | library |
What the plugin mirrors: library (default in v2.7+) mirrors your entire personal library — every top-level collection becomes a top-level folder, a PDF dropped at the root lands in Unfiled; collection is the legacy mode that mirrors a single chosen sync-root collection. Existing installs are automatically pinned to collection on upgrade; re-run setup to opt in to library. |
| pollInterval | 5 sec |
Seconds between scans. Adaptive backoff doubles this on idle scans (caps at 2× base). |
| fileTypes | pdf |
Comma-separated extensions. pdf,epub,djvu all work. |
| pdfStorageStrategy | stored |
Where PDFs live (separate from sync mode): stored (Zotero manages + syncs them), linked_watch_folder (Zotero links to the watch-folder file — saves Zotero Storage), or stored_plus_mirror (both). The prefs pane offers a guided "Reclaim Zotero Storage" / "Build/Repair Mirror" conversion. |
| importMode | stored |
Legacy, superseded by pdfStorageStrategy. linked is treated as linked_watch_folder. |
| postImportAction | leave |
After import: leave the source file, delete it, or move it to a sibling folder. |
| autoRetrieveMetadata | true |
Run Zotero's PDF metadata recognizer after import. Failures tagged _needs-review. |
| autoRename + renamePattern | true · {firstCreator} - {year} - {title} |
Template-based file rename. Available vars: {firstCreator}, {creators}, {year}, {title}, {shortTitle}, {DOI}, {itemType}, {publicationTitle}. |
| diskDeleteOnTrash | ask |
When you trash in Zotero: ask, plugin_trash (recoverable, default in dialog), os_trash, permanent, never. Only effective in Mode 3. |
| diskDeleteSync | auto |
When you delete a tracked file on disk: auto trashes the Zotero item (with bulk guard), never does nothing. |
| duplicateCheck | true |
Skip imports that match an existing item by hash → DOI → ISBN → fuzzy title (threshold 85). |
| smartRulesEnabled + smartRules | false · [] |
Toggle the rules engine and provide a JSON array of rules. Editor in prefs pane. |
| adaptivePolling | true |
Back off poll interval on quiet scans; reset on any non-empty scan. |
No. The plugin stamps a hash into each item's Extra field (watchfolder-hash:<sha256>). Even if the local tracking store is wiped, the library-side hash catches re-imports during the dedup pass.
Yes. The first-run baseline (B.1–B.7) reconciles your existing collection tree against the disk tree before any destructive op is allowed. It mkdirs empty collections, adopts files that already match by hash, and only then does any normal mode-3 propagation begin.
The first becomes the canonical record; the second becomes a "shadow" record with the same Zotero attachment key. The cascading-trash guard ensures only the canonical path is ever disk-deleted — Zotero will not trash the attachment because of a shadow's deletion.
The item still imports. It gets a _needs-review tag and stays in your inbox / target collection until you fix it manually. Rename templates with missing fields fall back gracefully.
More than 10 files or more than 20% of your tracked items in a single operation. Both _handleZoteroTrash (Zotero-side bulk trash) and _handleExternalDeletions (disk-side bulk rm) go through the same guard. Decline at the prompt and propagation is demoted to "mark missing".
Yes to both. The manifest declares strict_min_version: 6.999 and strict_max_version: 9.*. Primary development target is Zotero 8.
Your choice, via the PDF storage strategy (separate from the sync mode). Store in Zotero keeps PDFs in Zotero's storage and syncs them via Zotero Storage/WebDAV. Link from watch folder leaves PDFs in your folder and links Zotero to them — this saves Zotero Storage space, and your folder-sync tool (pCloud, Dropbox, Syncthing…) backs them up. Store + mirror keeps both. Either way, Zotero still syncs your metadata, notes, and highlights — those are independent of where the PDF files sit. If you're already storing PDFs in Zotero and want to free up storage, the prefs pane has a guided "Reclaim Zotero Storage" tool (it keeps annotated PDFs stored, and the old copies go to Zotero's recoverable trash).
A form-based smart-rules editor (the engine is in place; the editor is still a JSON window) and broader live-Zotero MCP coverage of less-common Mode 3 scenarios. None of these are release-blocking.
v2.8.4 is the current stable release: a redesigned Settings pane — a live status header plus a tabbed layout — on top of the v2.8.3 no-orphaned-records delete, the v2.8.2 two-way-delete prompt, the v2.8.1 audit-hardening pass, the v2.8.0 Check & Repair tool, the v2.7 whole-library mirror default and the v2.6.x data-safety wave. The bar to ship was: 929 unit tests passing across 27 files, live-Zotero MCP validation on Zotero 9.0.4, and no known data-loss path.
Plain ES modules under content/, bundled to an IIFE by esbuild,
loaded by the bootstrap. No frameworks. No build step heavier than esbuild.
# clone git clone https://github.com/josesiqueira/zotero-watch-folder cd zotero-watch-folder npm install # unit tests (929 passing, 27 files) npm test # build the bundle Zotero actually runs npm run bundle # content/index.mjs → dist/content/scripts/watchFolder.js npm run build # copies the rest of dist/ npm run package # produces the .xpi + sha256 + update.json # reload in a running Zotero (via MCP bridge) zotero_plugin_reload { pluginId: "watch-folder@zotero-plugin.org" }
Module layout, invariants, and "don't touch without understanding" notes live
in CLAUDE.md. Test overview in test/README.md.