Skip to content

Safe-by-default writes

Writes are preview-first, with no exceptions. Every command that changes your library or your vault previews by default, applies only under --yes, and counts what it plans against --max-changes. That covers the bulk fixers and importers (items enrich, tags audit fix, items duplicates resolve, import apply, import doi, import file, import url, and the generic import <resource>), every single-item create (items new, import pmid, import arxiv, import isbn), the single-resource CRUD commands (items create/update/delete/restore, collections create/update/delete), and every vault command (vault push, vault pull, vault sync, vault resolve); workflow run extends the same contract across a multi-step plan. Read the contract once and you never have to guess which command is dangerous.

zotio write-safety lifecycle Preview-first write command lifecycle with gates, explicit apply, Web API write, local mirror replay, append-only journal, and reversible undo. Safe by default: preview, gate, apply, journal, undo Every zotio write command starts as a reviewable plan; nothing mutates until explicit consent passes safety gates. preview / local gates apply / Web API journal / undo 1. COMMAND tags audit fix items enrich --missing-doi 2. PLAN (preview) plan / result envelope default: preview only NO changes yet 3. GATES --max-changes cap 500 · agent 50 --allow-destructive irreversible --require-fresh fresh mirror REFUSE loud failed gate = no write 4. Decision --yes provided? No: STOP preview is the final output no yes 5. APPLY explicit mutation applies the change --agent does NOT auto-apply --dry-run always wins WRITE to Zotero Web API update / delete / enrich / tags with configured api_key REPLAY to local mirror read-your-writes fresh re-audit without sync 6. RECORD append-only JOURNAL journal list journal show journal undo <run-id> UNDO reverses tag rename collection membership REFUSES rest merges, deletes field overwrites loud, not silent Hook: safe by default — preview, gate, apply, journal, undo.

The contract

  • Preview is the default. You get a plan/result envelope with zero changes. --yes applies; --dry-run always wins.
  • --agent does not auto-apply. Agent mode sets JSON + non-interactive defaults, but a write still needs an explicit --yes.
  • No command is exempt. Every write in this CLI needs --yes, and --dry-run overrides it everywhere — including inside a workflow run preview, which previews each step by injecting --dry-run. The vault commands keep their own per-note report rather than the generic envelope, but they obey the same gate.
  • Gates cap the blast radius. --max-changes defaults to 500 (50 under --agent); irreversible ops (merge, permanent delete, empty-trash) refuse to run without --allow-destructive.
  • Read-your-writes. A write applied through the mutation envelope is replayed into the local mirror immediately and the post-write item state comes back in the envelope, so a re-audit sees the fix with no follow-up sync. Replay is deliberately conservative: scalar field edits and tag/collection membership are replayed, while creates, trash, and structural edits are left for the next sync to reconcile authoritatively.
  • Journaled, and honest about undo. Runs applied through the mutation envelope are recorded append-only (journal list / journal show), including the single-resource CRUD commands and partially-rejected batches (a 100-item create where one element is refused still journals the 99 that landed). Some gated writes deliberately sit outside the envelope and so are not journaled: the vault commands, which keep their own report. journal undo <run-id> reverses only what it can invert losslessly — tag and collection membership toggles, and creates (reversed by trashing the created item) — and loudly refuses everything else (merges, deletions, field overwrites) rather than guessing. A journal entry is therefore an audit record, not a promise of reversibility: items delete is journaled but not undoable, because the journal records what a run did, not a full pre-image of what it replaced. Recover a deleted item from Zotero's trash with items restore.
  • One writer at a time. Applying a write takes the installation-wide advisory lock (~/.zotio/.writer.lock). export snapshot and collections bundle instead lock their canonical output path, so runs writing to different directories stay parallel, and a vault write holds both the installation lock and its vault-path lock. Writers do not queue: a second concurrent writer in the same scope exits 9 immediately and is safe to retry once the first finishes. Reads and previews are never blocked, so parallelize those and serialize applies. See Architecture decisions › Single-writer concurrency.
  • Partial success is loud. Zotero answers a batched write with HTTP 200 even when it rejected some elements. Those rejections are reported with their source index and message, and the command exits 13 (degraded) — never 0.

Across a whole workflow

The contract scales from one command to a plan. zotio workflow run previews every mutating step by default and applies the whole plan on a single --yes; every mutation in the run shares one journal run ID (journal list --workflow <id>), and an interrupted run resumes from a checkpoint without replaying a write whose outcome is uncertain. See Workflows & triggers.

Where writes land

Writes split by intent — new items prefer the keyless local desktop connector; everything else routes to the Zotero Web API and needs a key.

zotio hybrid routing architecture Hero diagram showing zotio routing reads locally without an API key, writes to Zotero Web API with a key, external metadata providers, local-only desktop commands, local version reads before writes, and replay into the SQLite mirror. zotio hybrid routing Local-fast reads, key-gated cloud writes, and an immediately refreshed local mirror. READ WRITE EXTERNAL LOCAL-only Semantic colors are shared across README diagrams. READ plane Local-first Local Zotero API :23119 Zotero desktop running items list/get collections annotations fulltext Local SQLite mirror Synced cache for compound reads search items audit stats Desktop connector New-item create path create items · attachments · PDFs prefers local · else Web API local reads/creates need no key · cloud writes do CLOUD write path Zotero Web API api.zotero.org Auto-routed mutation target items update/delete/move enrich · tags audit fix collections create/update vault push · journal undo LOCAL-only plane Files · desktop · introspection vault sync items open doctor capabilities EXTERNAL plane Metadata providers CrossRef · OpenAlex Semantic Scholar Unpaywall · OpenCitations zotio routing + safety layer one CLI surface reads new items writes replay → mirror enrich · import local ops

The capabilities reference lists the operation, write target, destructiveness, and requirements for every command. See Authentication for key setup.

Example

zotio tags audit fix --agent            # preview: the merge plan, zero changes
zotio tags audit fix --agent --yes      # apply
zotio journal list                      # find the run id
zotio journal undo <run-id>             # reverse the tag renames