Skip to content

Command line

There are two subcommands. check audits references; cache inspects or empties the store check reads and writes. bibaudit --version prints the installed version and exits.

bibaudit check references.bib
bibaudit check notes/**/*.md --bibliography library.bib
bibaudit check ~/Zotero/zotero.sqlite
bibaudit check . --format json --output audit.json

check takes one or more paths: bibliographies (.bib), documents (.qmd/.md), Zotero libraries (zotero.sqlite, a CSL-JSON export, or the word local for a running Zotero), or directories containing them. A directory is walked recursively, except that a directory holding a zotero.sqlite is read as a library instead; dot-prefixed components, _site, _freeze, node_modules and any *.suggested.bib are skipped during that walk. Naming a .suggested.bib file on the command line still audits it — that is exactly what reviewing a proposal before applying it requires. The paths matched against those rules are relative to the directory being scanned, so a project that lives under a dot-directory is not silently emptied of every file.

Flag Default
-b, --bibliography PATH none bibliography used to resolve [@citekey] references in documents; repeatable. Discovered from Quarto/YAML front matter when omitted
--format {text,json} text report format
-o, --output PATH stdout write the report here instead of stdout
--fail-on LIST BAD-ID,FIELD-MISMATCH,RETRACTED,UNCONFIRMED,WRONG-WORK comma-separated verdicts that make the exit code non-zero. Pass an empty string to never fail

--fail-on is split on commas, stripped and upper-cased; it is not validated against the known verdicts, so a misspelled name quietly fails nothing. Whatever it resolves to governs the exit code, the closing PASS/FAIL banner, the summary.failing_verdicts and summary.exit_code fields of the JSON report, and which verdict groups the terminal report prints — one policy, stated in every place the reader might read it from. A verdict named here brings its references into the report with their citekeys, locators and issue lines, so nothing can fail the run without saying which entry did it; a verdict left out of it keeps printing if it was going to print anyway, so --fail-on '' still shows a retracted citation. See verdicts for what each name means and in CI for the exit codes. One thing it cannot switch off: a [@citekey] used in a document with no matching bibliography entry exits 1 on its own, whatever --fail-on says and whichever format was asked for.

Registries

Flag Default
--mailto EMAIL none contact address sent to Crossref and NCBI; puts requests in Crossref's polite pool. No account or key is needed
--no-corroborate off skip PubMed corroboration, and PubMed lookups for entries resolved by a PMID
--no-search off do not try to confirm entries that carry no identifier
--no-retraction-check off skip the independent Retraction Watch / PubMed expression-of-concern check
--no-isbn off do not resolve books through Open Library
--no-europepmc off drop Europe PMC from the identifier-less search
--no-openalex off drop OpenAlex from the identifier-less search
--offline off use only cached registry responses
--refresh off ignore cached responses and refetch
--cache-dir PATH per-user cache directory where registry responses are cached
--cache-ttl DAYS 90 how long a cached registry response stays valid
--timeout SECONDS 30.0 per-request timeout

--mailto is appended to the User-Agent that every request carries, in the form bibaudit/<version> (+https://github.com/lorenzoFabbri/bibaudit); mailto=you@example.org. Crossref grants its polite pool — lower latency, priority during load-shedding — to requests carrying a contact address in exactly that form, so omitting it is a silent slowdown rather than an error, which is how it stays wrong for a long time. It is not an API key, and there is no API key anywhere in this tool: nothing bibaudit sends to any registry is a credential, and no service it consults requires an account.

--no-corroborate constructs no PubMed client at all, which costs two different things on two kinds of entry.

On a DOI-bearing entry it removes PubMed from the resolution path: what you lose is the independently curated second opinion — the one that catches a defect in Crossref's own deposit rather than agreeing with it. What you do not lose on an otherwise default run is the requests: the retraction check has its own PubMed leg, handed the identical DOI list, so the same E-utilities queries are issued either way — by the corroboration pass, or, once this flag removes it, by the retraction pass that had been reading them back out of the cache the corroboration pass filled. --help says the flag halves request volume; that holds only alongside --no-retraction-check, which is also what it takes to stop querying PubMed at all.

On an entry resolved by a PMID it removes the only registry that could have answered: Crossref and DataCite are keyed on DOIs and have nothing to say about a PMID, and the retraction leg is keyed on DOIs too, so it does not pick this up. Such an entry is then reported UNCHECKED — nothing was verified — and not BAD-ID, which takes a response from PubMed with no citation under that number in it. It is the distinction --no-isbn rests on below, and it is what stops a decision not to look from reading as evidence about the bibliography.

--no-search stops entries carrying no identifier at all — no DOI, no PMID, no ISBN — from being looked up by title and author. An entry storing any of the three is not among them: it has an identifier, it is resolved by it, and this flag never reaches it. The flag does not quieten the rest — in a run where every registry answered, such an entry is reported UNCONFIRMED, a failing verdict, having consulted nothing. It stops the attempt to clear them, not the consequence of their not being cleared.

--no-retraction-check turns off the independent pair: Retraction Watch's own export and PubMed's expression-of-concern cross-reference. It does not turn off retraction reporting. A retraction the publisher deposited in Crossref's updated-by linkage, or that MEDLINE flags with PT - Retracted Publication, is read off the record itself and still fails as RETRACTED. What goes unnoticed is a retraction that was never deposited in either place — the gap the independent check exists for — and an expression of concern, which NLM records as ECI rather than as a PT and which this flag stops being read on the DOI path and the PMID path alike.

Most of that narrower coverage is stated rather than left to be noticed: every reference nothing was found against carries a status/not-asked finding naming retraction-watch, the run prints it beside the banner, and consulted records retraction-watch as not-asked on every reference in the run. A reference some source did report a retraction for carries the retraction instead — there is no gap left to state once the answer arrived. status/retraction-unverified is a different finding and stays what it is: a source that could not be reached.

The ECI half is the part no line states. PubMed answered — it returned the citation every other field was compared against — so consulted says answered and nothing names the field that went unread. That is the one gap this flag opens silently, and retraction says why there is no fourth consultation state for it. It covers the four sources in full as well.

--no-isbn means Open Library is never constructed, so no book is resolved through it — neither by ISBN nor by the title/author search used for books with no identifier at all. A book with no identifier then loses the one candidate source organised around books. A book that stores only an ISBN carries an identifier that nothing was asked about, and is reported UNCHECKED: nothing was verified, and the run says so rather than treating your decision not to look as evidence the book does not exist.

This flag cannot fail a book for existing

BAD-ID requires a registry that answered "I do not hold this". Switching the only competent registry off produces no such answer, so it cannot produce that verdict. A malformed ISBN is different and still fails: a bad check digit is arithmetic on the stored value, and no registry's answer could change it. See verdicts.

--no-europepmc and --no-openalex affect only the confirmation path for entries with no identifier; neither is consulted for a DOI. They are not symmetrical. Europe PMC is curated separately from Crossref and indexes material Crossref does not — preprints, some grey literature, agency reports — so dropping it loses candidates nothing else in the tool would find. OpenAlex substantially re-crawls Crossref's deposits, so it never corroborates a match on its own and is consulted for discovery only; dropping it narrows what can be found, never what would have been accepted. Every candidate, whichever source produced it, is judged by the identical bar. When every enabled search source fails for an entry, the search reports ignorance rather than an empty result, so a total outage leaves that entry UNCHECKED rather than UNCONFIRMED.

--offline never opens a socket. A cached answer is replayed — including a cached 404, which is a fact about a work and as worth replaying as a successful response — and anything uncached is reported UNCHECKED rather than fetched. A cached response older than --cache-ttl is a miss like any other, so the same command re-run outside that window derives an honestly narrower report, not a different one. --refresh cannot be honoured without a network: given both, the cache is still read.

--cache-dir defaults to $XDG_CACHE_HOME/bibaudit when that variable is set on any platform, otherwise ~/Library/Caches/bibaudit on macOS and ~/.cache/bibaudit elsewhere. It relocates everything the run caches, the parsed Retraction Watch index included: that index lives in a retraction-watch/ subdirectory of whichever cache root the run was given, so bibaudit cache info counts it and bibaudit cache clear discards it. It keeps its own seven-day lifetime, which --cache-ttl does not change and --refresh does not shorten. Ninety days is right for bibliographic fields, which do not change under a fixed DOI; seven is right for a status, which does. The export the index is parsed from is fetched past this cache rather than stored in it, so --cache-ttl cannot outlive the seven days and answer the refetch they ask for.

--timeout bounds one request. A logical lookup is retried — one attempt plus four retries, backing off 1, 2, 4 and 8 seconds, or by whatever a Retry-After header asks for up to a minute — after which the registry counts as unreachable, which is ignorance (UNCHECKED), never a missing work (BAD-ID).

Report detail

Flag Default
-v, --verbose off include informational verdicts (cosmetic, drift, registry artifacts)
--show-suppressed off list differences that were suppressed and why
--no-citekey-check off skip checking that every [@citekey] resolves to a bibliography entry
--suggest off write <name>.suggested.bib and <name>.suggested.diff beside each checked .bib with a fillable gap

By default the text report prints only the verdict groups a reader must act on — the failing set plus DISPUTED, where two curated registries contradict each other and only a person can settle it — and within those, only issues above informational severity. --verbose prints every group and every issue.

--show-suppressed lists what was suppressed and why, beneath the entry it belongs to, and works without --verbose: it pulls in the entries that actually carry a suppressed difference even though their verdict group is not printed by default. The counts appear in the summary either way, because a report that quietly drops what it decided not to tell you is not an audit. See adjudicating a difference.

--no-citekey-check drops both the unresolved-key list and the exit 1 it causes. The check runs only when documents were scanned and at least one bibliography entry was read, so it is already inert on a bare .bib run.

--suggest fills only fields an entry has none of, writes <name>.suggested.bib and <name>.suggested.diff beside the source, and never opens the source for writing. Its notices go to stderr, not to the report stream, so a --format json stdout stays parseable; it is silent when there was nothing to propose. See proposing fixes.

Thresholds

Flag Default
--title-wrong-work R 0.55 title similarity below which the identifier may point at a different work
--title-mismatch R 0.85 title similarity below which titles are reported as disagreeing

Both are ratios on the same similarity function. --title-wrong-work can sit as low as it does only because a title scoring below it is cross-checked against the author list before WRONG-WORK is declared: one fuzzy score is not enough evidence to accuse a bibliography of citing the wrong paper. Lowering --title-mismatch quietens disagreements at the cost of missing real ones; raising it does the reverse. The relaxed bands used for books and chapters, the band above which a title difference drops to informational severity, and the bar for confirming an entry with no identifier are not exposed on the command line.

bibaudit cache

bibaudit cache info
bibaudit cache clear

info prints the cache directory, how many responses are stored and their total size; clear empties it. Both accept --cache-dir, which has to match the one check was given. If the directory cannot be created, both say so on stderr rather than reporting a working, empty cache — an unusable cache and an empty one otherwise produce identical output, and the difference is exactly what someone diagnosing why --offline reports everything UNCHECKED needs to see.