Claude Code Academic Research Setup (and Codex): Catch Fabricated Citations With Crossref and Run a Reviewer Before Submission
Claude Code academic research setup: integrity rules in AGENTS.md, a Crossref script that flags fabricated DOIs, a read-only reviewer and a pre-submission run.
Claude Code academic research sessions tend to fail in one place first: the bibliography. Ask an agent to “add a citation for the plant-closure literature” and it will hand back a tidy BibTeX entry with authors, a journal, a year and a DOI. Nothing in that entry tells you whether the paper exists, or whether the DOI belongs to the paper the entry describes. Codex fails the same way. A sharper prompt does not close that gap. A checked process does: a project folder with written rules, a script that asks Crossref about every DOI and fails loudly, and a referee pass that cannot touch your manuscript.
This guide builds that process for one illustrative project, paper:labor-shocks-01, a labor-economics working paper with a BibTeX file, a data folder and a figure script. Every file is printed in full. You need Claude Code or Codex installed and signed in, Python 3, Git and a draft with a references file. You do not need to write code yourself. You need to run it and read what it prints.
The occasion is a tool. On Nov 13, 2026, Jeremy Nguyen (@JeremyNguyenPhD) shared Ingar30/reviewer on X under the line “Claude Code/Codex for academic research”, describing it as a multi-agent reviewer for research papers. It is the last gate in this workflow. Everything before it is what makes its report worth reading.
Before you start: what this setup needs
The workflow has three layers, and they carry different kinds of weight. Written rules in AGENTS.md and CLAUDE.md shape what the agent tries to do, but Anthropic’s documentation is explicit that Claude treats CLAUDE.md as context, not enforced configuration (How Claude remembers your project). Checks with exit codes, like the citation script below, give you a pass or fail that does not depend on the agent’s account of its own work. File permissions and tool allowlists stop an action outright. Each rule in this guide is backed by the strongest layer available for it, and the guide says which layer that is.
That distinction matters more in research than in most software work. A wrong function fails a test. A wrong citation sits quietly in a PDF until a referee, an editor or a reader looks it up. The only reliable defence is a check you run yourself, on every entry, every time the bibliography changes.
Tick these off before you create any files:
- [ ] Claude Code or Codex is installed and signed in. The reviewer step later needs one authenticated CLI, signed in with
claude auth loginorcodex login; you do not need both. - [ ] Python 3 is on your PATH. The citation script uses only the standard library, so there is nothing to install for it.
- [ ] Python 3.12 or newer is available if you plan to run the Ingar30 reviewer, which states that requirement in its README.
- [ ] Git is installed, so
refs.biband the drafts are versioned and every agent edit can be inspected and reverted. - [ ] Your references are in a BibTeX file with one field per line and each entry closing with
}on its own line, which is how most reference managers export. - [ ] You have an email address to send to Crossref with each request, which places you in Crossref’s polite pool.
- [ ] You know whether your manuscript may leave your machine. The review step sends parsed text to Anthropic or OpenAI; the section on the reviewer explains when that is acceptable.
Set up the paper project folder
The folder layout does two jobs. It tells the agent where things belong, and it gives you places to put the boundaries that the rules refer to. Here is the full tree for paper:labor-shocks-01 once this guide is finished:
labor-shocks-01/
├── AGENTS.md shared rules for Codex and Claude Code
├── CLAUDE.md imports AGENTS.md, adds Claude-only lines
├── .claude/agents/
│ └── paper-reviewer.md read-only referee subagent
├── check_refs.py Crossref DOI check
├── verified_manually.txt keys a human verified by hand
├── refs.bib the bibliography
├── sources/ PDFs of papers you actually read
├── notes/ reading notes and ai-use.md
├── data/raw/ original data, write-protected
├── data/derived/ outputs of your scripts
├── code/ analysis and figure scripts
├── figures/ exported PDF and PNG figures
└── drafts/ main.tex and the exported PDF
Create it with these steps:
- Create the folders and start a repository. On macOS, Linux or WSL:
mkdir -p labor-shocks-01/{sources,notes,data/raw,data/derived,code,figures,drafts,.claude/agents}
cd labor-shocks-01
git init
- Copy your original data files into
data/raw/, then remove write permission from them withchmod -R a-w data/raw. This is the enforcement layer for the “never edit raw data” rule: an agent that tries to overwrite a file there gets a permission error from the operating system, whatever its instructions say. On Windows,attrib +R data\raw\* /Ssets the read-only attribute on the files. When you genuinely need to replace a raw file, restore write permission yourself, replace it and protect it again. - Create a
.gitignorecontaining.refcache.json(the script’s local cache) and, if the papers you collected may not be redistributed,sources/*.pdf. - Create an empty
notes/ai-use.md. The rules below ask the agent to append one line per session, which becomes your record for the venue’s disclosure policy. - Commit:
git add -A && git commit -m "Project skeleton". Every later agent change now shows up ingit diff.
The sources/ folder deserves a sentence of its own. It is the agent’s allowed universe of quotations: if a passage is quoted in the draft, the PDF it came from should be in sources/, with a page number. If you want the agent to read those PDFs, the page-range and conversion advice in our PDF reading guide for Claude Code and Codex applies unchanged, so this guide does not repeat it.
Write the integrity rules once in AGENTS.md
Claude Code and Codex look for different instruction files, and you want one set of rules, not two copies that drift. The documented way to get that is to write the rules in AGENTS.md and make CLAUDE.md import it. Codex reads AGENTS.md files from the project root down to the current directory (OpenAI, Custom instructions with AGENTS.md). Claude Code reads AGENTS.md directly only when there is no CLAUDE.md in the working directory or above it, and it recommends a CLAUDE.md that starts with an @AGENTS.md import when you need both files (Anthropic memory docs). The import route works on every Claude Code version that supports imports, so use it.
This is the complete AGENTS.md for the example project. Replace the email address with your own; everything else can stay as written:
# Project: paper:labor-shocks-01
## What this project is
Working paper on local labor-market shocks. Manuscript: drafts/main.tex.
Bibliography: refs.bib. Figures are produced only by scripts in code/.
## Integrity rules (apply to every task)
- Never invent citations, data, coefficients, sample sizes or results.
If a number is not in data/derived/ or a script output, write TODO instead.
- Never add an entry to refs.bib unless it has a DOI, or the user names the
source and confirms it exists.
- After any change to refs.bib, run the citation check below and report its
full output. Never call a reference verified unless its line starts with OK.
- Never edit verified_manually.txt. Only the authors add keys to it.
- Never edit, move or delete anything in data/raw/. Write outputs to data/derived/.
- Never edit image files in figures/. Change the script in code/ and rerun it.
- Quote sources only from PDFs in sources/ and give the page number.
- When you describe a paper you have not opened, say so.
## AI-use log
Append one line per session to notes/ai-use.md: date, tool, task, files changed.
The authors use this log for the venue's AI-disclosure policy.
## Commands
- Citation check: python3 check_refs.py refs.bib --mailto research@example.edu
- Figures: python3 code/make_figures.py
And this is the complete CLAUDE.md. The first line pulls in every rule above; the rest applies only to Claude Code:
@AGENTS.md
## Claude Code
- Use the paper-reviewer subagent for any request to review or referee the draft.
- Ask before running any command that writes outside this folder.
Then confirm both tools actually loaded the rules:
- In Claude Code, start a session in
labor-shocks-01/and run/context all. Expected observation:CLAUDE.mdappears under Memory files. The importedAGENTS.mdcontent is part of it. - In Codex, run
codex --ask-for-approval never "Summarize the current instructions."from the project root. OpenAI’s guide gives the expected result: Codex quotes the items from your instruction files before proposing work. You should see the integrity rules echoed back. - If Codex shows nothing, check that the file is not empty and that no
AGENTS.override.mdsits higher in the tree, the two causes OpenAI lists first. - Keep the file short. Anthropic suggests staying under 200 lines per CLAUDE.md, and Codex stops adding instruction files once the combined size reaches
project_doc_max_bytes, 32 KiB by default.
Why write “run the check and report its full output” instead of “make sure citations are real”? Because the second instruction asks the agent to judge its own work, and the first asks it to produce evidence you can read. The rules are still context, not a lock. That is why the raw data folder is write-protected at the file-system level, and why the citation rule ends in a script whose exit code you check yourself. For more on writing instructions that a model follows reliably, see our Claude Code prompting guide.
Build check_refs.py against the Crossref REST API
Crossref is the registration agency behind most journal-article DOIs, and its REST API is free to use with no sign-up (Crossref REST API). Two endpoints do the work here. /works/{doi} returns a single metadata record for a Crossref DOI. /works/{doi}/agency returns the registration agency for a DOI, for example Crossref or DataCite. Adding a mailto parameter puts your requests in the polite pool. Crossref’s tips page adds two operating rules the script follows: cache results instead of repeating requests, and back off when you see HTTP 429, which means you are being rate limited.

Save this as check_refs.py in the project root. It is complete; nothing else is needed:
#!/usr/bin/env python3
"""check_refs.py: check every DOI in a BibTeX file against the Crossref REST API.
Usage: python3 check_refs.py refs.bib --mailto you@university.edu
Exit 0 only when every entry is OK, or its key is listed in verified_manually.txt.
"""
import argparse
import difflib
import json
import re
import sys
import time
import unicodedata
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
API = "https://api.crossref.org/works/"
CACHE = Path(".refcache.json")
MANUAL = Path("verified_manually.txt")
THRESHOLD = 0.90
ENTRY = re.compile(r"@(\w+)\s*\{\s*([^,\s]+)\s*,(.*?)\n\}", re.S)
def field(body, name):
"""Return a BibTeX field value written on its own line(s), or ''."""
pattern = r"\b" + name + r"\s*=\s*[{\"](.*?)[}\"]\s*,?\s*\n"
match = re.search(pattern, body + "\n", re.S | re.I)
return " ".join(match.group(1).split()) if match else ""
def norm(text):
"""Lower-case; strip HTML tags, LaTeX commands, accents and punctuation."""
text = re.sub(r"<[^>]+>", " ", text)
text = re.sub(r"\\[`'^\"~=.]", "", text)
text = re.sub(r"\\[a-zA-Z]+", " ", text)
text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode()
text = re.sub(r"[^0-9a-z]+", " ", text.lower())
return " ".join(text.split())
def get_json(url):
"""GET a URL, backing off on HTTP 429. Returns (status, data); 0 = network error."""
for attempt in range(4):
try:
with urllib.request.urlopen(url, timeout=30) as resp:
return resp.status, json.load(resp)
except urllib.error.HTTPError as err:
if err.code == 429 and attempt < 3:
time.sleep(5 * 2 ** attempt)
continue
return err.code, None
except (urllib.error.URLError, TimeoutError, ValueError):
return 0, None
return 429, None
def lookup(doi, mailto, cache):
"""Return [status, crossref_title, agency] for one DOI, using the cache."""
if doi in cache:
return cache[doi]
quoted = urllib.parse.quote(doi, safe="/")
polite = "?mailto=" + urllib.parse.quote(mailto)
status, data = get_json(API + quoted + polite)
title, agency = "", ""
if status == 200:
title = (data.get("message", {}).get("title") or [""])[0]
elif status == 404:
a_status, a_data = get_json(API + quoted + "/agency" + polite)
if a_status == 200:
agency = a_data.get("message", {}).get("agency", {}).get("id", "")
result = [status, title, agency]
if status in (200, 404):
cache[doi] = result
time.sleep(0.2)
return result
def verdict(doi, bib_title, mailto, cache):
if not doi:
return "NO DOI", "add a DOI or verify by hand"
status, cr_title, agency = lookup(doi, mailto, cache)
if status == 200:
score = difflib.SequenceMatcher(None, norm(bib_title), norm(cr_title)).ratio()
if score >= THRESHOLD:
return "OK", f"title match {score:.2f}"
return "TITLE MISMATCH", f"bib: {bib_title} | crossref: {cr_title}"
if status == 404 and agency and agency.lower() != "crossref":
return "NOT CROSSREF", f"agency: {agency}; resolve https://doi.org/{doi}"
if status == 404:
return "NOT FOUND (404)", doi
return "ERROR", f"HTTP {status or 'network'}; rerun later"
def main():
parser = argparse.ArgumentParser()
parser.add_argument("bibfile")
parser.add_argument("--mailto", required=True, help="your email, for the polite pool")
args = parser.parse_args()
text = Path(args.bibfile).read_text(encoding="utf-8")
manual = set(MANUAL.read_text().split()) if MANUAL.exists() else set()
cache = json.loads(CACHE.read_text()) if CACHE.exists() else {}
checked = failures = 0
for kind, key, body in ENTRY.findall(text):
if kind.lower() in ("comment", "string", "preamble"):
continue
doi = field(body, "doi")
doi = re.sub(r"^(https?://(dx\.)?doi\.org/|doi:\s*)", "", doi, flags=re.I).strip()
status, detail = verdict(doi, field(body, "title"), args.mailto, cache)
if status != "OK" and key in manual:
status, detail = "OK (manual)", "listed in verified_manually.txt"
checked += 1
if not status.startswith("OK"):
failures += 1
print(f"{status:<16} {key:<22} {detail}")
CACHE.write_text(json.dumps(cache, indent=1))
print(f"\n{checked} checked, {failures} need attention")
sys.exit(1 if failures or checked == 0 else 0)
if __name__ == "__main__":
main()
How the script decides, in the order it runs:
- It reads each BibTeX entry and pulls out the
doiandtitlefields, stripping a leadinghttps://doi.org/ordoi:from the DOI. - No DOI field means
NO DOI. The script cannot verify what has no identifier, so a human has to. - It URL-encodes the DOI before building the request. The slash between prefix and suffix is kept; characters such as spaces,
<,>,;and#, which turn up in some older DOI suffixes, are encoded so they cannot break the URL. - A record from
/works/{doi}means the DOI is registered with Crossref. The script then compares titles after removing case, LaTeX commands, accents, HTML tags and punctuation, and calls itOKat a similarity of 0.90 or above,TITLE MISMATCHbelow that. - An HTTP 404 triggers a second request to
/works/{doi}/agency. If another agency answers, the verdict isNOT CROSSREF. If nothing answers, it isNOT FOUND (404). - Anything else, including a network failure or rate limiting that persists through three back-offs, is
ERROR. Errors are never cached and never count as a pass. - Keys listed in
verified_manually.txtturn a failure intoOK (manual). The exit code is 1 if any line is not OK, and also if no entries were parsed at all, so a broken export cannot masquerade as a clean bibliography.
One honest limitation: the Crossref documentation pages selected for this guide describe the endpoints but do not print a sample response. The script expects the record’s title under message.title and the agency name under message.agency.id. Confirm that once by opening https://api.crossref.org/works/10.1128/mbio.01735-25, the example Crossref itself uses, in a browser. If the fields were ever elsewhere, every lookup would report TITLE MISMATCH or NOT FOUND. The script fails loud, not silent, which is the property you want from a gate.
This is the same habit our package-install gate for coding agents applies to dependencies: a model-generated name is a claim, and a registry lookup is the check.
Run the worked example on six references
To trust the script, watch it fail on purpose. The fixture below is entirely illustrative: the authors, journals and the second, third and fifth titles are invented to exercise each branch. Do not cite them. Save it as refs.bib in a scratch copy of the project, or append it temporarily to your real file.
@article{smith2021-ok,
title = {REPLACE with the Crossref title printed in step 2},
doi = {10.1128/mbio.01735-25},
}
@article{wrongtitle-mismatch,
author = {Lee, Ana},
title = {Minimum Wages and Teen Employment in Border Counties},
journal = {Journal of Labor Shocks},
year = {2019},
doi = {10.1128/mbio.01735-25},
}
@article{fabricated-doi-404,
author = {Okafor, Ben and Ruiz, Clara},
title = {Plant Closures and Local Wage Spillovers},
journal = {Regional Labor Review},
year = {2024},
doi = {10.1128/not-a-real-doi-labor-shocks-01},
}
@misc{datacite-dataset,
title = {County Employment Panel, 2000-2024},
doi = {10.5281/zenodo.1234567},
}
@book{book-nodoi,
author = {Doe, Jane},
title = {Local Labor Markets},
year = {2010},
}
@techreport{ilo-report-manual,
title = {Regional Employment Outlook},
institution = {International Labour Office},
year = {2020},
}
Then create verified_manually.txt with a single line, ilo-report-manual, to stand for a report you checked against the publisher’s own page.
Work through it in this order:
- Run
python3 check_refs.py refs.bib --mailto research@example.eduand thenecho $?to print the exit code. - Read the first
TITLE MISMATCHline. The text aftercrossref:is the record’s real title for10.1128/mbio.01735-25. Paste it into thetitlefield ofsmith2021-ok, replacing the REPLACE line. - Delete
.refcache.jsononly if you want fresh lookups; otherwise rerun the same command. The cache means the second run sends no repeat requests for DOIs it already resolved. - Compare your output with the table and the expected listing below.
This is what the first run should look like. It is an illustrative expected observation, not a captured run; <record title> stands for the title Crossref returns, and the agency name is whatever the agency endpoint reports:
TITLE MISMATCH smith2021-ok bib: REPLACE with the Crossref title printed in step 2 | crossref: <record title>
TITLE MISMATCH wrongtitle-mismatch bib: Minimum Wages and Teen Employment in Border Counties | crossref: <record title>
NOT FOUND (404) fabricated-doi-404 10.1128/not-a-real-doi-labor-shocks-01
NOT CROSSREF datacite-dataset agency: datacite; resolve https://doi.org/10.5281/zenodo.1234567
NO DOI book-nodoi add a DOI or verify by hand
OK (manual) ilo-report-manual listed in verified_manually.txt
6 checked, 5 need attention
| Input | Action | Expected result (illustrative) |
|---|---|---|
ref:smith2021-ok, doi 10.1128/mbio.01735-25, title pasted from the record in step 2 |
Rerun python3 check_refs.py refs.bib --mailto research@example.edu |
OK with title match 1.00 or close to it; this line no longer counts toward the failures total |
ref:wrongtitle-mismatch, the same real DOI with the title “Minimum Wages and Teen Employment in Border Counties” |
Same command | TITLE MISMATCH, with both titles printed side by side: a real DOI attached to a paper it does not identify |
ref:fabricated-doi-404, doi 10.1128/not-a-real-doi-labor-shocks-01 |
Same command; the script also queries /works/{doi}/agency |
NOT FOUND (404) when neither endpoint returns a record; the entry is treated as fabricated |
ref:datacite-dataset, doi 10.5281/zenodo.1234567 (illustrative repository dataset DOI) |
Same command | NOT CROSSREF if the agency endpoint names an agency other than Crossref, otherwise NOT FOUND (404); both exit 1 |
ref:book-nodoi, a 2010 book entry with no doi field |
Same command | NO DOI with the hint “add a DOI or verify by hand” |
ref:ilo-report-manual, no DOI, key listed in verified_manually.txt |
Same command | OK (manual); it does not count as a failure because a human vouched for it |
| Whole fixture after the step 2 fix | echo $? after the run |
1, with the summary 6 checked, 4 need attention; it reaches 0 only when every line starts with OK |
The success condition for the real bibliography is the last row: an exit code of 0. Until then, the draft is not ready to send, however confident the agent sounds.
Decide what to do with each verdict
A verdict is a prompt for a decision, and some of those decisions must stay with you. Use this table when the script reports something other than OK:
| Verdict | What it means | What you do | Can the agent resolve it alone? |
|---|---|---|---|
| OK | The DOI is registered with Crossref and the titles agree | Keep the entry | Yes |
| TITLE MISMATCH | The DOI is real but names a different paper, or the titles differ in wording | Open https://doi.org/<doi>. If the record is the paper you meant, correct the title. If not, delete the entry and find the real source |
No. A human picks which paper the sentence relies on |
| NOT FOUND (404) | Neither endpoint knows the DOI | Treat the reference as invented until you hold the paper. Find it in sources/ or on the publisher’s page, or remove the claim it supports |
No |
| NOT CROSSREF | Another agency, such as DataCite, registered the DOI | Resolve it at doi.org, confirm the landing page matches, then add the key to verified_manually.txt yourself |
No |
| NO DOI | Books, chapters, reports and older papers often lack one | Check the library catalogue or the physical copy, then add the key to verified_manually.txt |
No |
| ERROR | Network failure or rate limiting | Wait and rerun. Never submit with an ERROR line | Yes, by rerunning |
When you ask the agent to add a citation, phrase the request so the check is part of the task: “Find the DOI for the paper in sources/okafor2024.pdf, add a refs.bib entry, run the citation check and show me its full output.” The agent can do the lookup and the editing. The verdict comes from the script, and the decision on anything other than OK comes from you. That division is also why the AGENTS.md rules forbid the agent from editing verified_manually.txt: the file records human judgment, and an agent that could write to it could approve its own work.
Add a read-only paper-reviewer subagent
A referee that can edit the manuscript is a co-author, not a referee. Claude Code subagents let you enforce that difference with a tool allowlist rather than an instruction. Subagents are Markdown files with YAML frontmatter; only name and description are required; tools restricts what the subagent can use; and model accepts an alias such as sonnet or opus, a full model ID, or inherit (Create custom subagents). A project subagent lives in .claude/agents/. Our guide to custom subagents covers the general mechanics; this section gives the research version.
Save this as .claude/agents/paper-reviewer.md:
---
name: paper-reviewer
description: Read-only referee for the manuscript in drafts/. Use when asked to review, referee or critique the paper before submission.
tools: Read, Grep, Glob
model: inherit
---
You are a skeptical referee for an economics working paper. You cannot edit
files, and you must not ask for edit access.
When invoked:
1. Read the manuscript in drafts/ and list every empirical claim with its location.
2. For each claim, find the table, figure or script output in data/derived/,
figures/ or code/ that supports it. If you cannot find one, mark it UNSUPPORTED.
3. List every citation key used in drafts/ that is missing from refs.bib.
4. Flag numbers that differ between the abstract, the text and the tables.
5. Do not judge whether a reference is real. Tell the user to run check_refs.py.
Report by priority, with file and line for every item:
- Must fix before submission
- Should fix
- Consider
Each consequential choice in that file, and why it is there:
tools: Read, Grep, Globis an allowlist. Anthropic’s example of a read-only reviewer uses the same pattern, and its documentation notes that a subagent restricted this way cannot edit or write files. Bash is left out too, so the reviewer cannot shell out and modify files that way.- There is no
memoryfield. The docs state that enabling memory automatically gives the subagent Read, Write and Edit so it can manage its memory files, which would undo the read-only design. model: inheritruns the referee on whatever model your session uses. Change it toopusfor a stronger pass orsonnetto spend less of your allowance;/tasksshows which model a running subagent uses.- The prompt tells the reviewer not to rule on citation authenticity. That job belongs to the script, and a model asserting “this reference looks real” is the failure this whole guide is built to avoid.
Then install and test it:
- Save the file. If
.claude/agents/did not exist when your session started, restart Claude Code; a running session does not detect a newly created agents directory. - Optionally run
claude plugin validate .claude/agents(Claude Code v2.1.233 or later) to catch frontmatter that does not parse. Files with broken YAML are skipped silently in a normal session. - Invoke it explicitly: type
@and pickpaper-reviewer (agent)from the typeahead, or type@agent-paper-reviewer review drafts/main.tex before submission. Expected observation: a tool call row such aspaper-reviewer(...)appears in the transcript and a prioritised report comes back. - Test the boundary. Ask: “Use the paper-reviewer agent to fix the typo in the abstract.” Expected observation: the subagent reports that it cannot edit files. Afterwards,
git statusshows no modified files. - Commit
.claude/agents/paper-reviewer.mdso co-authors get the same referee.
The subagent file format is Claude Code’s. For Codex, the AGENTS.md rules still apply in every session, and the multi-agent referee pass in the next section runs with --backend codex. If you want Codex itself held to read-only during a review conversation, use its sandbox settings, which our Codex approval modes and sandbox guide explains.
Run the multi-agent reviewer before submission
The Ingar30/reviewer repository describes itself as a reproducible multi-agent reviewer for academic economics papers, using Codex or Claude Code, under the MIT license. Its README lays out the pipeline: it preprocesses the PDF locally into text, tables, figures, citations and cross-references; runs a parser-quality preflight; selects every reviewer whose remit is plausibly relevant; validates each reviewer’s structured JSON; and has an editor assemble a final report. It is tuned for economics. Papers in other fields will run, but expect specialists such as identification or power analysis to be skipped or to return little.

The roster is large, and that matters for your usage allowance. Eight universal reviewers always run, covering cross-references, source consistency, claim-evidence alignment, literature, reference integrity, copyediting, abstract-conclusion consistency and models and notation. Eleven conditional specialists join unless a high-confidence classification shows their remit is absent, so an uncertain paper gets all 19. The wrapper runs up to four reviewer agents at a time.
Run it with these steps:
- Decide whether the paper may leave your machine. The README is direct about it: the PDF parser is local, but the review is not. Parsed manuscript text goes to OpenAI with Codex or to Anthropic with Claude Code, and search-enabled reviewers may send manuscript-derived queries to web search. The README says not to review a confidential paper unless its disclosure terms permit those transmissions. Your own working paper, with your co-authors’ agreement, is the intended use. A manuscript you received to referee for a journal is not yours to send.
- Install uv. On Windows the README gives
winget install --id astral-sh.uv -e; for macOS and Linux it points to uv’s installation guide. Open a new terminal and runuvx --version. - Export the draft to
drafts/main.pdf. - Run a prerequisite check without a review:
uvx --from git+https://github.com/Ingar30/reviewer.git economics-paper-reviewer --backend claude --check
The README notes that --check does not verify model access or remaining quota, so a pass here only means the install is sound.
- Start the review. The workspace sits outside the Git repository so reviewer intermediates never get committed:
uvx --from git+https://github.com/Ingar30/reviewer.git economics-paper-reviewer \
--backend claude \
--pdf "drafts/main.pdf" \
--paper-id labor-shocks-01 \
--workspace "../labor-shocks-01-review"
Expected observation: the selected backend and model print at startup, and the final report lands at ../labor-shocks-01-review/outputs/labor-shocks-01/report.md. Use --backend codex for Codex.
- If the run stops, repeat the same command with
--resume-incomplete. It reuses validated completed reviewers when preflight and routing finished; keep the same workspace, paper ID, PDF and model. - For a re-review months later, pin the code you used: replace the source with
git+https://github.com/Ingar30/reviewer.git@COMMIT, using the commit hash from your first run, so a changed reviewer does not explain a changed report.
Cost and version notes from the README are worth reading before the first run. The Claude backend defaults to Opus 5.5 and needs Claude Code 2.1.280 or later; --model claude-sonnet-5-5 needs 2.1.284 or later and generally uses less subscription allowance, though its review quality has not been benchmarked by the maintainer. Reviews draw on your subscription, and the README advises keeping Usage credits or Extra usage off to stay inside it. Live reviews were tested on Windows x64 with Python 3.12; other platforms had no live-review validation at the time of writing.
Read the report alongside your own checks, not instead of them. Its literature and reference reviewers use web search and return cannot_verify when evidence is unavailable; treat that as unverified, exactly like an ERROR line from check_refs.py. Turn the must-fix items into tasks for your main session, rerun the citation check after any bibliography change, and rerun the referee subagent on the revised draft. If you keep the drafting session, the subagent review and the multi-agent run going at once, the one-boss approach to running several agents helps you see which session is waiting on you.
Export figures with savefig settings journals accept
Figures are the other place agents quietly alter results, usually by “tidying” an image instead of the code that drew it. The AGENTS.md rule keeps every figure reproducible from a script; this is the script’s export step for the example project:
# code/make_figures.py
import csv
import matplotlib.pyplot as plt
years, rates = [], []
with open("data/derived/employment_by_year.csv", newline="") as f:
for row in csv.DictReader(f):
years.append(int(row["year"]))
rates.append(float(row["employment_rate"]))
fig, ax = plt.subplots(figsize=(6.5, 4))
ax.plot(years, rates, marker="o")
ax.set_xlabel("Year")
ax.set_ylabel("Employment rate (%)")
# Vector PDF for the manuscript: the format comes from the .pdf extension.
fig.savefig("figures/fig1_employment.pdf", bbox_inches="tight", pad_inches=0.02)
# Raster copy for slides and submission-portal previews.
fig.savefig("figures/fig1_employment.png", dpi=300, bbox_inches="tight", pad_inches=0.02)
plt.close(fig)
What each setting does, from the matplotlib savefig reference, and how to check the result:
- With no
formatargument, matplotlib infers the format from the file extension, so.pdfproduces a vector PDF that scales without blurring and.pngproduces a raster image. dpisets the resolution in dots per inch. It decides the sharpness of the PNG; the default'figure'uses the figure’s own DPI. Use the value your journal’s artwork guidelines ask for.bbox_inches="tight"trims the saved area to the figure’s content, andpad_inchessets the margin kept around it when trimming.- Run
python3 code/make_figures.py, open both files and zoom in on the PDF. Expected observation: axis labels stay sharp at any zoom, and no label is cut off at the edge. - Commit the script and the outputs together, so
git logties every figure to the code and data that drew it.
Troubleshoot the setup
Most failures fall into a short list. Match the symptom first:
| Symptom | Likely cause | Fix |
|---|---|---|
Every entry reports TITLE MISMATCH, even ones you trust |
The response fields are not where the script expects, or your titles carry heavy LaTeX | Open https://api.crossref.org/works/10.1128/mbio.01735-25 and check where the title sits; compare one entry by hand |
| One correct entry scores just under 0.90 | Subtitle, abbreviation or math markup differences | Verify it by hand and add the key to verified_manually.txt; do not lower the threshold for everything |
ERROR HTTP 429 |
Crossref is rate limiting you | The script already backs off three times. Wait, confirm --mailto is set, rerun; the cache skips DOIs already resolved |
ERROR HTTP network |
Offline, proxy or timeout | Fix connectivity and rerun. Never submit with an ERROR line |
0 checked, 0 need attention and exit 1 |
Entries are not one field per line, or do not close with } on its own line |
Re-export the BibTeX from your reference manager |
A data-repository DOI reports NOT FOUND (404) |
The agency lookup returned nothing either | Resolve it at doi.org by hand, then list the key in verified_manually.txt |
| The agent says references are verified but the script disagrees | Rules are context, not enforcement | Trust the exit code. Ask for the full output, not a summary |
| Claude cannot find the subagent | The agents folder was created after the session started | Restart Claude Code |
| The subagent edited a file | tools was misspelled or omitted, so it inherited every tool |
Field names must match exactly; Claude Code ignores unknown fields silently. Run claude --debug and search the log for does not recognize (v2.1.296 or later) |
| Codex ignores the rules | An AGENTS.override.md higher up, an empty file, or the 32 KiB limit |
Run the summarize command from the AGENTS.md section and remove the override |
The reviewer --check passes but the run fails |
--check does not verify model access or quota |
Confirm you are signed in and have allowance left, then resume with --resume-incomplete |
Recover when an agent edits refs.bib
Agents edit bibliographies in bulk: reformatting fields, merging duplicates, “completing” missing DOIs. Some of that is useful and some of it invents things. Git turns the review into a routine:
- After every session that touched references, run
git diff refs.biband read every added or changed line. - Run
python3 check_refs.py refs.bib --mailto research@example.edu. Any non-OK line on a new entry means the entry does not go in. - To throw away all bibliography changes from the session, run
git restore refs.bib, which restores the last committed version. - To keep some changes and drop others, stage interactively with
git add -p refs.bib, accept the hunks you have checked, commit, thengit restore refs.bibto discard the rest. - If the agent changed several files and you want to roll back within a Claude Code session, the checkpoint and
/rewindmechanics in our checkpoints guide cover what is tracked and what is not; Git remains the record of last resort. - Never accept an agent-added reference that has not passed the script or been verified by you and listed in
verified_manually.txt. - Before submission, read
notes/ai-use.mdagainst your venue’s policy on AI assistance and write the disclosure the venue asks for. The log exists so that statement is accurate rather than reconstructed from memory.
Run this loop on every revision, not only the last one. A fabricated reference added in draft two and carried untouched into draft six is still fabricated, and the script costs one command to rerun.
Sources
- Jeremy Nguyen (@JeremyNguyenPhD) on X, “Claude Code/Codex for academic research”, Nov 13, 2026: x.com/JeremyNguyenPhD/status/2121200381317570777
- Ingar30/reviewer README on GitHub: github.com/Ingar30/reviewer
- Crossref, REST API documentation: crossref.org/documentation/retrieve-metadata/rest-api/
- Crossref, Tips and tricks for the REST API: crossref.org/documentation/retrieve-metadata/rest-api/tips-for-using-the-crossref-rest-api/
- Anthropic, Create custom subagents (Claude Code docs): code.claude.com/docs/en/sub-agents
- Anthropic, How Claude remembers your project (Claude Code docs): code.claude.com/docs/en/memory
- OpenAI, Custom instructions with AGENTS.md (Codex docs): developers.openai.com/codex/guides/agents-md
- Matplotlib, matplotlib.pyplot.savefig reference (3.11.2): matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.savefig.html

