Prompt & Done · REFCHECK pack
Ten sources from Claude. One was retracted.
Claude can hand you a reference list where every paper exists and one of them has been retracted, or where a link points at a different paper. A citation skill makes Claude look every reference up before you cite it.
1 The prompt
Put your reference list (with DOIs) in refs.txt, start Claude Code in that folder, and send:
Use the citation-management skill to check every reference in refs.txt against Crossref and PubMed. Tell me which are real, which point to the wrong paper, which don't exist, and which are retracted. Then give me a corrected list I can safely cite.In our test Claude loaded the skill, then queried Crossref and PubMed itself with curl. It took 53 s and $0.18 (API-equivalent) for the 10 Sonnet references, 90 s and $0.23 for the 10 Haiku references. Any reference list with DOIs works, not only science.
2 Get the skill (free, MIT)
npx skills add K-Dense-AI/scientific-agent-skillsThat is the install line from the repo's README: github.com/K-Dense-AI/scientific-agent-skills (177 skills; we used citation-management, with literature-review and paper-lookup also in the folder). Manual install for Claude Code, as we did it:
mkdir -p .claude/skills && cp -r scientific-agent-skills/skills/citation-management .claude/skills/3 The test we ran (22 Oct 2026)
No tools, no web, no skills: claude -p --tools "" with this prompt, six runs (Sonnet 5.5 and Haiku 5.5, three topics):
Write a short literature review (about 150 words) on <your topic>. End with a numbered reference list of 10 peer-reviewed papers in APA style, each with its DOI. Output only the review and the references.We then looked up every DOI in Crossref and OpenAlex ourselves. Of 60 citations, 54 were real and correct, 2 were retracted (the same ChatGPT-in-education meta-analysis, retracted 22 April 2026), 2 had a DOI that does not exist, and 2 had a DOI that points at a different paper. Four of the six runs had at least one bad reference. With the skill, Claude flagged the retraction and fixed the wrong DOIs; both corrected lists re-checked clean (9 of 9 and 10 of 10).
4 The 60-line checker we used to double-check
Independent of the skill: reads a numbered reference list, looks each DOI up in Crossref and OpenAlex and prints REAL, FAKE, MISMATCH or RETRACTED. Save as check_refs.py and run python3 check_refs.py refs.txt.
check_refs.py
#!/usr/bin/env python3
"""Independent citation check used for the reel test (not the skill's own code).
For every numbered reference line in a text file: pull the DOI, ask Crossref (does it exist, what title,
any retraction notice) and OpenAlex (is_retracted), and compare the title with the one Claude wrote."""
import json, re, sys, time, urllib.parse, urllib.request, difflib
UA = {'User-Agent': 'promptanddone-citation-test/1.0 (mailto:[email protected])'}
def get(url):
for i in range(3):
try:
return json.load(urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=25))
except urllib.error.HTTPError as e:
if e.code == 404: return None
time.sleep(2)
except Exception:
time.sleep(2)
return 'ERR'
def norm(s): return re.sub(r'[^a-z0-9 ]', '', s.lower())
def main(path):
out = []
for line in open(path):
m = re.match(r'\s*(\d+)\.\s+(.*)', line)
if not m: continue
n, body = m.groups()
d = re.search(r'10\.\d{4,9}/[^\s<>"]+', body)
doi = d.group(0).rstrip('.,;)') if d else None
t = re.search(r'\(\d{4}[a-z]?\)\.\s+(.+?)\.\s+[\*_A-Z]', body)
said = t.group(1) if t else body[:80]
row = dict(n=int(n), doi=doi, said=said, status='NO DOI')
if doi:
cr = get('https://api.crossref.org/works/' + urllib.parse.quote(doi))
if cr is None: row['status'] = 'FAKE (DOI does not exist)'
elif cr == 'ERR': row['status'] = 'UNKNOWN (network)'
else:
msg = cr['message']; real = (msg.get('title') or [''])[0]
row['real_title'] = real
row['sim'] = round(difflib.SequenceMatcher(None, norm(said), norm(real)).ratio(), 2)
row['status'] = 'REAL' if row['sim'] > .6 else 'MISMATCH (DOI is a different paper)'
row['crossref_retraction_flag'] = real.upper().startswith('RETRACTED') or any(u.get('type') == 'retraction' for u in msg.get('update-to', []) or []) or bool(msg.get('relation', {}).get('is-retracted-by'))
oa = get('https://api.openalex.org/works/doi:' + urllib.parse.quote(doi))
row['openalex_is_retracted'] = (oa or {}).get('is_retracted') if isinstance(oa, dict) else None
if row['crossref_retraction_flag'] or row['openalex_is_retracted']:
row['status'] = 'RETRACTED' if row['status'] == 'REAL' else row['status'] + ' + RETRACTED'
out.append(row)
print(json.dumps(out, indent=1, ensure_ascii=False))
from collections import Counter
print(Counter(r['status'] for r in out), file=sys.stderr)
main(sys.argv[1])
5 Honest limits
This checks that a DOI exists, matches the title and has no retraction notice in Crossref or OpenAlex. It does not check that a paper supports the sentence it is cited for; read the papers you rely on. Crossref does not capture every retraction. Six runs on three topics is a small test, not a rate: most references were fine. The skill's own scripts check DOI registration only; the retraction catch came from Claude reading Crossref's records.