Skip to contents

The zot command line operates this package without writing R. It parses arguments, calls one exported function and maps the result and the package’s condition classes to standard output and an exit code. Every rule it applies belongs to the package, so the same operation is available from R.

The command is a separate component: it lives in the product repository under cli/ and is installed from an authorized checkout together with this package. Installing the package from an archive does not install it, and this article describes the interface rather than shipping it.

The three lanes

Lane Verbs Effect on a library
Read doctor, libraries, census, collections, items, get, local items\|query\|records\|files, semantic, identity twins\|match None
Build clean name\|title\|payload, payload template\|check\|crossref, plan retitle\|refile\|move-create\|move-trash, preview None; plan and preview verbs write local batch files
Mutation approve, execute, verify Only execute writes, and only under an approved preview

zot --help lists them with one line each, and zot VERB --help prints that verb’s flags. Use the full two-word name for a subverb, for example zot plan retitle --help. --json prints an operation result as one document on standard output; diagnostics go to standard error. Help and version output remain text.

Local paths and result files

The CLI has no --root flag and does not discover a project directory. Relative input and output file paths resolve from the working directory of the command, including --sql-file, --records, --candidates, --items, --payload, --plan, --preview, --approval, --ledger and --out. Selecting or moving a JSON file does not change that base. Absolute paths keep their meaning. --library selects a Zotero library, not a local directory.

For local verbs, --db selects a SQLite file; the default is ~/Zotero/zotero.sqlite. For semantic, --db selects a Chroma directory; the default is ~/.config/zotero-mcp/chroma_db. Explicit relative database paths also use the working directory. semantic --python selects the companion interpreter; use its absolute path to avoid executable-search ambiguity. Its default is ~/.local/share/uv/tools/zotero-mcp-server/bin/python3. doctor probes these defaults and has no --db or --python override.

Only the four plan verbs, preview and approve accept --out. They require a new destination in an existing directory, then write one JSON batch document there. The writer uses <destination>.tmp beside the destination and renames it into place. That companion path is part of the write; use a separate destination for each concurrent batch writer.

execute --ledger is different: it creates or updates the ledger at the exact path supplied, with a sibling .tmp during each replacement. The parent directory must already exist. An existing ledger is reusable for the same preview digest; a different digest is refused. plan move-trash and verify only read their --ledger file. No command recursively clears a project output directory.

All verbs also report a result on stdout. With --json, that result is an envelope containing ok, verb, data and meta, or an error on an exception. It is not a batch file and is not automatically accepted as an input record array. --items for plan retitle and --records for identity twins and the move plans take JSON arrays of records with the fields described below; identity match takes arrays of candidate and reference records. Prepare those arrays from the result data before passing them back. Payload inputs take the payload object itself. Shell redirection such as > result.json is controlled by the shell and has none of the CLI’s new---out checks.

Inputs, results and effects by verb

The table covers the 27 verbs in the CLI’s own verb table. Optional selectors and limits are listed by each verb’s help. Except for the batch and ledger destinations shown here, results go to stdout; the command does not save a second report automatically.

Verb Input or source Result and destination
doctor Installed package, environment, one Web API read when credentials are available, local database and semantic paths. Availability table. Opens SQLite read-only; checks semantic paths without launching Python.
libraries Configured user’s accessible libraries via the Web API. Personal/group identifiers, names and library versions.
census --library and optional repeated --collection NAME=KEY; API reads. Top-level, Trash and named-collection counts.
collections --library; API reads. Collection identifiers, hierarchy paths, item counts and versions.
items --library, repeated --key or listing selectors/filters. No selector lists the library. Items or versions, with listing metadata or missing keys. A listing returns one page by default; --all requires --max.
get Positional /SUBPATH, --library, optional repeated --query NAME=VALUE. GET status, headers and body under the library’s API prefix. This path is not a filesystem path.
local items, local records SQLite --db, optionally --library; without a library selector these read all local libraries. Non-trashed items or bibliographic identity records. No database copy or update.
local query Exactly one of --sql or --sql-file, optional positional --param values, SQLite --db. Query rows through a read-only connection; supply a read-only SQL statement.
local files SQLite --db, optional repeated --key and --md5. Attachment metadata and existing local paths. Imported files resolve under storage/<key>/ beside the selected database; supported absolute linked paths remain as stored. It does not download, move or copy attachments.
semantic Positional query, existing Chroma --db, companion --python, optional --limit. Item keys, group IDs, titles and distances. Launches Python to query zotero_library; does not build or refresh the index. There is no library selector.
identity twins --records: array with item keys, titles and identity evidence. Duplicate-candidate pairs and their identity verdicts.
identity match --candidates and --records: arrays of API records or identity objects. Candidate/reference pairs and evidence verdicts.
clean name, clean title Positional text. Cleaned text. A filename argument is treated as text; no file is renamed.
clean payload --payload: JSON payload object. Cleaned payload on stdout; the input file is preserved.
payload template --type; Zotero template API reads. Allowed fields, creator types and empty template.
payload check --payload: JSON object; template API reads. Validation result or a refusal with the problems found; no item is created.
payload crossref Positional DOI; Crossref lookup, without Zotero credentials. Sanitized create payload on stdout; no item is created.
plan retitle --items: array with itemKey, library, title. Title patches using the fixed title cleaner, saved as a plan at new --out; also reports the plan on stdout.
plan refile --library, repeated --key, --add-to; reads current API memberships. Collection-membership patches at new --out, skipping absent items and memberships already present.
plan move-create --records: array with itemKey, hasFile, payload; --to-library and --collection. Create plan at new --out. Records carrying files are refused; this does not transfer attachments.
plan move-trash Original --records in the same order as the create plan, its --ledger, and --from-library. Trash plan at new --out for originals whose corresponding create-<row> ledger entry is done. No remote Trash action occurs here.
preview --plan batch file and --describe. Validated preview with digest, counts and rows at new --out; stdout reports its summary. No API request.
approve --preview, the displayed --digest, and --notes. Approval record at new --out; stdout reports digest, notes and time. It records authorization supplied by the caller; it does not authenticate the approver or execute the batch.
execute --preview, matching --approval, and --ledger; configured API access. Applies approved patch/create/Trash rows remotely, updating the local ledger before and after each attempt. Stdout reports counts, ledger path and unfinished rows.
verify --preview, --ledger, optional --full; API reads. Counts and read-back verdicts on stdout. Does not rewrite the ledger or modify Zotero. Default verification samples up to ten completed patch rows; --full checks every completed row, including create and Trash.

The plan, preview and approval files use the package’s kind-tagged JSON batch format. A ledger contains digest and items; it is maintained by execution. The separate --out files let the owner inspect each stage before authorizing execution. Changing an output path and rerunning a plan or preview rebuilds it from its current inputs.

Exit codes

Code Meaning
0 Success. Zero results, or a 404 from get, is a verdict rather than an error
1 Operational failure: an exhausted transport, an unavailable plane
2 Usage: an unknown verb or flag, a missing value, an input file that is not there
3 Refusal-class condition: configuration, plan, approval, bound or unresolved-ledger error; see the effect limits below
4 Incomplete: a batch whose rows are not all done, or a verification that did not confirm

An exit code classifies the result; it does not by itself establish whether remote effects occurred. Validation of credentials, plan and approval before execution can refuse without a write. However, a failure to rename the ledger after an API write is also a plan-class error mapped to code 3. The phrase “refused without effects” in older help is therefore not a blanket guarantee. Current help states that codes do not establish write effects. A previous attempt can also mean an unresolved remote outcome. Inspect the reported cause, ledger and current library state before resuming.

Code 4 means incomplete execution or verification, not proof of a successful remote write: verify performs reads only, and an execution can record failed rows. A JSON envelope with ok: true means the runner returned a result; doctor, execute and verify can return that envelope with a nonzero exit code. Check the exit code and the result’s statuses together.

From a plan to a verified batch

The mutation lane is the guarded flow of vignette("guarded-workflow") with each step in its own process, so the owner can read and approve the exact rows between them:

zot plan retitle --items items.json --out plan.json
zot preview --plan plan.json --describe "Normalize reviewed titles" --out preview.json
# show preview.json to the owner, then retype the digest it reports
zot approve --preview preview.json --digest <sha256> \
            --notes "owner approved the displayed batch" --out approval.json
zot execute --preview preview.json --approval approval.json --ledger ledger.json
zot verify --preview preview.json --ledger ledger.json --full

approve reads the preview under the digest given on the command line: a preview whose rows changed since it was displayed cannot be approved by mistake. execute recalculates that digest again before its first request, resumes an interrupted run through the same ledger path, and stops on a row whose outcome is unknown. verify re-reads what the ledger recorded.

The batch is not a transaction. Execution records attempt before each row, then its result; completed rows are skipped on resume. A retained attempt stops execution for reconciliation, while other unfinished statuses are attempted again. A failed read-back can follow a successful remote write, so an unfinished row is not evidence that nothing happened. Earlier rows are not rolled back when a later row fails. Verification does not repair the ledger or authorize a retry.

The files are the package’s own batch documents, so R can read and write the same ones:

library(data.table)
#> 
#> Attaching package: 'data.table'
#> The following object is masked from 'package:base':
#> 
#>     %notin%
library(zot)

Items <- data.table(
  itemKey = c("A", "B"),
  library = "user",
  title = c("Reviewed book (1).pdf", "Existing title")
)
Plan <- ztRetitle(Items, transform = ztCleanTitle)

PlanFile <- tempfile(fileext = ".json")
zotWriteBatch(Plan, PlanFile)
Preview <- zotPreview(zotReadBatch(PlanFile), describe = "Normalize reviewed titles")
Preview$digest
#> [1] "1737f2bbf3c2706d92827a746c5299a38e518e066d343dd596632c04ae8abb9e"

A preview read back from disk recalculates its digest, so an edited file is refused instead of executed under the earlier approval:

PreviewFile <- tempfile(fileext = ".json")
zotWriteBatch(Preview, PreviewFile)

Document <- jsonlite::read_json(PreviewFile, simplifyVector = FALSE)
Document$rows[[1]]$payload$title <- "Something else"
jsonlite::write_json(Document, PreviewFile, auto_unbox = TRUE, null = "null")

zotReadBatch(PreviewFile)
#> Error:
#> ! preview rows no longer match their stored digest: /tmp/RtmpJ0EYsX/file42fb378104bc.json

Credentials and planes

The command reads ZOTERO_LIBRARY_ID and ZOTERO_API_KEY from its process environment, which R fills from ~/.Renviron; there is no flag for the key, because command lines are visible to other processes. zot doctor reports the package, the credentials, the Web API, the local database and the optional semantic sidecar, naming the missing variable or path instead of guessing. Local and semantic reads are advisory planes: the Web API remains the truth a write is verified against.