alterlab-citation-verifier — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited alterlab-citation-verifier (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
The headline existence-verification skill: given a bibliography in any common form, it proves entry-by-entry whether each reference actually exists by querying public scholarly APIs, then maps each result to the canonical AlterLab citation-hallucination taxonomy. It is the deterministic, network-grounded companion to the LLM-driven integrity_verification_agent — where that agent uses WebSearch + judgment, this skill uses authoritative API records and a reproducible Python script, so every verdict rests on retrieved records and the same input and source responses yield the same verdicts.
Verify references with the script, not from memory. A model checking citations against what it "remembers" shares the training data that produced the hallucination in the first place, so a fabricated reference that feels right passes undetected; only an external record settles existence.
Verify the citations in references.bib
Check whether these DOIs resolve to the papers I cited
Audit my bibliography for fabricated / hallucinated references
Does this reference list contain any fake citations or retractions?→ Run scripts/verify_citations.py over the bibliography, read the JSON, then present a verdict table grouped by severity. State the offline/degraded status explicitly if the network or a source was unavailable.
| Scenario | Use Instead |
|---|---|
| Writing or revising the paper itself (its citation-check mode formats citations) | alterlab-paper-writer |
| Finding papers, extracting metadata, or generating BibTeX for new sources | alterlab-citation-mgmt |
| Dead or redirected hyperlinks (link rot) in docs or a reference list | alterlab-link-health |
| Full pre-/post-review integrity gate (citation context, data, originality) | alterlab-research-pipeline |
| Holistic peer review of a manuscript with an editorial decision | alterlab-paper-reviewer |
Grading source quality or predatory venues belongs to alterlab-deep-research (source_verification_agent). For a whole-manuscript audit in Claude Code where every flag should be re-checked by independent agents, offer the packaged /alterlab-workflows:citation-audit workflow (alterlab-research-workflows, roughly 10–40 agents), which runs this skill's checks at scale.
For each bibliography entry the script:
references, including one-reference-per-line pastes), extracting title, authors, year, venue, DOI, and arXiv ID. A full reference that merely contains a DOI stays free-form, so its title and authors are kept for the Identifier Hijacking check.
and asks the doi.org Handle API whether a DOI is registered with any agency.
SequenceMatcher ratio, defaultthreshold 0.70; a main title that is near-identical to a record stored without its subtitle also counts) and computes author-surname overlap. Among the returned records it keeps the one that matches title, then authors, then year — so a same-titled paper by other authors or another edition does not win.
updated-by / update-tonotices of type retraction, withdrawal, or removal (from the publisher or, with source: retraction-watch, the Retraction Watch database that Crossref serves in its REST API since 2025) and OpenAlex is_retracted. Expressions of concern get a separate flag.
source_status map showing which sources answered, plus a repo-level summary.verdict (PASS / PASS_WITH_CONDITIONS / FAIL / UNVERIFIED).
| Source | Endpoint | Used for | Access notes |
|---|---|---|---|
| Crossref | api.crossref.org/works | DOI resolution, bibliographic search, retraction notices | mailto routes to the polite pool (10 req/s, 3 concurrent) |
| OpenAlex | api.openalex.org/works | DOI lookup + title search, is_retracted | mailto is ignored since Feb 2026. DOI lookups are free; searches draw on a daily budget shared per IP without a key (a free key gives 10× — set OPENALEX_API_KEY) |
| Semantic Scholar | api.semanticscholar.org/graph/v1 | DOI/arXiv resolution, title search | Keyless pool is shared and often returns 429; S2_API_KEY gives a dedicated 1 req/s |
| arXiv | export.arxiv.org/api/query | arXiv ID resolution, preprint title search | ≥ 3 s between calls (the script throttles) |
| doi.org Handle API | doi.org/api/handles/<doi> | Is the DOI registered with any agency (Crossref, DataCite, mEDRA…)? | Authoritative for DOI existence |
Keys are optional, read from the environment, sent as headers, and never written to the report (the report only records whether one was present). A source that errors (rate limit, 5xx) is recorded in source_status and is never counted as evidence that a work does not exist.
This skill answers "does the cited work exist, and does its identifier point to it?" From API metadata alone it cannot establish Semantic Hallucination (does the source support the claim?); scripts/claim_faithfulness.py gives an abstract-level SH triage (see below), and full-text claim verification is the research pipeline's claim_verification_protocol (Phase E).
Identical codes and definitions to alterlab-research-pipeline/agents/integrity_verification_agent.md (GPTZero × NeurIPS 2025; Ansari, 2026). Severity feeds the same SERIOUS / MEDIUM / MINOR scale used in the Integrity Report schema.
| Code | Name | Severity | Script trigger |
|---|---|---|---|
verified | — (exists, matches) | NONE | Title ratio >= threshold AND author overlap OK AND year consistent in >=1 authoritative source |
TF | Total Fabrication | SERIOUS | Cited DOI is not registered at doi.org, or cited arXiv ID does not exist on arXiv, and no close title match exists; OR (no identifier) no record matches title + authors while Crossref and OpenAlex or Semantic Scholar answered |
PAC | Partial Attribute Corruption | MEDIUM | Entry found but >=1 metadata field disagrees (year, author overlap < 50%, a cited identifier that does not resolve to it, or a garbled title whose closest record shares the authors — possible mashup) |
IH | Identifier Hijacking | SERIOUS | Cited DOI/arXiv ID resolved (method=id) but the resolved record's title is unrelated (ratio < threshold) |
PH | Placeholder Hallucination | SERIOUS | Unresolved template/placeholder ([CITATION NEEDED], \cite{}, et al., YYYY, TODO, forthcoming, in press) — caught pre-network |
SH | Semantic Hallucination | SERIOUS | Entry resolves but does not support its claim — advisory only here; asserted via claim_faithfulness.py / Phase E |
unverified | — (could not check) | MEDIUM | Offline, the sources needed for a decision errored, or a registered DOI that no index returned. Never treated as passing. |
A RETRACTED flag is attached (and severity bumped to SERIOUS) whenever Crossref or OpenAlex marks the matched work as retracted, independent of the existence verdict; an unrelated closest hit never lends its flag to the cited entry.
TF is a serious accusation, so the script only asserts it on authoritative evidence. When the deciding sources were rate-limited or down, the entry is unverified with the reason and source_status, and it goes to the fallback below.
verified, no SERIOUS/MEDIUM flags.PAC / MEDIUM metadata issues (fixable), and every entry was checked.TF / IH / PH / retraction).checked (e.g. an offline run or rate-limited sources). This is not a pass — re-run with network access or an API key, or resolve those entries manually.
Accept any of: a .bib file, a .txt list of DOIs/arXiv IDs, a pasted reference list, or inline text. The script auto-detects the format; override with --format bibtex|doi|freeform if detection is wrong.
uv run python skills/core/alterlab-citation-verifier/scripts/verify_citations.py \
path/to/references.bib \
--mailto <contact-email> \
--threshold 0.70 \
--out citation_report.jsonpath/to/references.bib may also be - (stdin) or inline text.--threshold tunes the fuzzy title-match ratio (0..1; default 0.70).--offline skips the network and emits unverified verdicts deliberately.--out to print the JSON report to stdout.export OPENALEX_API_KEY=… (free key from openalex.org/settings/api)and/or S2_API_KEY=… before large bibliographies — keyless OpenAlex searches share a small per-IP daily budget, and the keyless Semantic Scholar pool is busy.
The script uses requests if installed, else the Python stdlib (urllib), so it runs with no extra dependencies in a bare uv environment. Expect roughly 3–10 s per entry: it spaces arXiv calls 3 s apart and backs off on rate limits.
Parse summary.verdict and the per-entry verdict codes. Present:
verdict_counts, severity_counts).detail.TF / IH / PH: quote the cited entry and explain the evidence(e.g. "DOI 10.x resolved to an unrelated paper titled '…'", "DOI not registered at doi.org").
RETRACTED / EXPRESSION_OF_CONCERN flags, prominently.unverified entries: say which sources failed (source_status), relay themanual_instructions and any summary.notes (for example, an OpenAlex HTTP 429 means the keyless per-IP budget is spent — a free OPENALEX_API_KEY fixes it), and run the fallback below.
TF / PH → the reference must be removed or replaced; it does not exist.IH → the DOI/arXiv ID is wrong; find and substitute the correct identifier.PAC → correct the specific metadata field(s) named in detail; a possiblemashup needs the real source identified before it can stay.
RETRACTED → flag to the author; cite the retraction notice or drop the source.Failures are never swallowed into a pass:
NetworkUnavailable; the entry becomesunverified with a populated manual_instructions field.
source_status; if the sourcesneeded for a decision did not answer, the entry is unverified, not TF.
--offline forces every networked entry to unverified up front (placeholdersare still caught locally as PH).
UNVERIFIED whenever an entry is unverified andnothing SERIOUS was found.
For each unverified entry, re-run with connectivity or a key if possible; otherwise look it up with the plugin's crossref / openalex MCP tools when they are available (setup: mcp_setup.md under skills/core/references), then fall back to WebSearch with three distinct queries (exact title in quotes; title + first author; first author + venue + year) and, for a DOI, open https://doi.org/<DOI>. If a record is found, report the verdict it supports and say it came from the fallback. If nothing is found, classify the entry as TF (NOT_FOUND). There is no "difficult to verify" outcome: the Lin et al. mashup this skill was built around slipped through three integrity rounds precisely because it was parked in that gray zone and never searched.
scripts/claim_faithfulness.pyExistence is half of the gate; a real paper cited for something it never said (the "Frankenstein" pattern) passes every existence check. For (claim, DOI) pairs, claim_faithfulness.py fetches the cited abstract (Crossref, then OpenAlex) and returns support / contradict / unsupported:
uv run python skills/core/alterlab-citation-verifier/scripts/claim_faithfulness.py \
--claim "<the sentence as written>" --doi <doi>
uv run python .../claim_faithfulness.py --input pairs.json --json # batch
uv run python .../claim_faithfulness.py --input pairs.json --tier llm --jsonheuristic tier is lexical and deliberately abstains(unsupported) rather than guessing support; it cannot see role reversals or wrong numbers. The llm tier asks the model set by ALTERLAB_MODEL (see shared/model_env.md) and falls back to the heuristic, flagged, if the claude CLI is unavailable.
abstract_only: true). unsupported means theabstract does not establish the claim — non-coverage, not refutation. Report contradict as SH; send unsupported claims that matter to full-text checking.
"the citation supports this sentence" are different findings.
{
"tool": "alterlab-citation-verifier/verify_citations.py",
"version": "1.1.0",
"summary": {
"total": 2,
"verdict": "FAIL",
"verdict_counts": {"verified": 1, "TF": 1, "PAC": 0, "IH": 0, "PH": 0, "SH": 0, "unverified": 0},
"severity_counts": {"SERIOUS": 1, "MEDIUM": 0, "MINOR": 0},
"citation_integrity_score": 0.5,
"fabrication_risk_score": 0.5,
"retracted": 0,
"source_errors": {"openalex": 1},
"notes": ["OpenAlex rate limit: the keyless daily budget shared by this IP is spent ..."]
},
"entries": [
{"ref_id": "walters2023", "verdict": "verified", "severity": "NONE",
"title_ratio": 1.0, "author_overlap": 1.0, "matches": [{"source": "crossref"}],
"source_status": {"crossref": "record", "openalex": "record", "semanticscholar": "record", "arxiv": "no_record"}},
{"ref_id": "ghostpaper2021", "verdict": "TF", "severity": "SERIOUS",
"detail": "DOI 10.xxxx/... is not registered with any DOI agency (doi.org Handle API)..."}
]
}citation_integrity_score and fabrication_risk_score (both 0..1) align with the Integrity Report schema fields of the same name, so the report can feed alterlab-research-pipeline's integrity gate directly.
unverified, name the failing sources from source_status; do not imply a pass.
RETRACTED flags? Surface them even on otherwise-verified entries.IH? Confirm the detail shows an id-resolved mismatch,not a loose title-search coincidence (the script enforces this distinction).
any unverified without SERIOUS → UNVERIFIED)?
unverified entry get the WebSearch fallback and a final verdict?alterlab-research-pipeline/agents/integrity_verification_agent.md — canonicalFive-Type Taxonomy, compound-deception patterns, and the Lin et al. (2020) mashup case study this skill is built to catch.
alterlab-research-pipeline/references/claim_verification_protocol.md — Phase Efull-text claim-vs-source verification (beyond the abstract-level SH triage here).
shared/schemas/integrity_report.schema.json — the integrity-report shape whosecitation_integrity_score / fabrication_risk_score this skill mirrors.
examples/caught_hallucination_walkthrough.md — end-to-end run on a real mashup.bibliographic citations generated by ChatGPT. Scientific Reports, 13, 14045. https://doi.org/10.1038/s41598-023-41032-5
taxonomy of 100 fabricated citations at NeurIPS 2025* (arXiv:2602.05930). https://arxiv.org/abs/2602.05930
Crossref API* [Blog post]. Crossref. https://www.crossref.org/blog/retraction-watch-retractions-now-in-the-crossref-api/
Part of the AlterLab Academic Skills suite.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.