ref-manager

Tutorial

Round-trip with ReadCube Papers

Read in Papers, keep the library as the authority. You'll open a PDF in the app, export a batch Papers can import with your note and tags attached, import it yourself, and bring your highlights back. Commands used: /ref:note, /ref:open, /ref:export-papers, and /ref:pull-annotations. The last step covers what /ref:fetch does and doesn't do with Papers.

The journey

you Claude Code <LIBRARY> Papers.app database never written 1234 /ref:open 38000101 resolve acquired PDF open -a Papers <pdf> /ref:export-papers … read-only snapshot (SQLite backup API) items: PMID / DOI → already_known references.bib · PDFs · manifest destination + "import through Papers' own UI" import references.bib in the app (you, not the plugin) read · highlight · margin notes /ref:pull-annotations 38000102 snapshot again $.user_data.annotations[] upsert annotations.json
ref-manager writes a file, or hands the OS a file to open read-only snapshot of Papers' database you, in the app
Outbound is files only, and inbound is reads only. Neither direction writes Papers' live database, whose actions_queue is a sync queue that writes could corrupt. For the field-by-field mapping, see the Papers handoff section of the paper directory anatomy figure.

The example uses project tdcs-review, batch wm-batch, and placeholder PMIDs 38000101 and 38000102, both screened included. Outputs labelled scratch library are real. There, --papers-db pointed at a synthetic SQLite file with Papers' items(id, collection_id, json) shape, never at the real library.

Steps

  1. Write the note you want to carry over

    notes.md is your content, and /ref:note is the only command that writes it. Each entry is appended with a timestamp.

    output from a scratch library

     /ref:note 38000101 "Strongest RCT so far; sham blinding reported in supplement."
    appended to …/lib/papers/38000101/notes.md
  2. Open one PDF in Papers

    /ref:open hands the acquired PDF to the OS with open -a Papers, the same as double-clicking it in Finder. It's the only command that touches the app itself. With no PDF on file, it says so without failing:

    output from a scratch library

     /ref:open 38000101
    {
      "pmid": "38000101",
      "status": "no_pdf_acquired",
      "message": "no acquired PDF for this paper yet — run /ref:fetch or /ref:attach first"
    }

    Once /ref:fetch or /ref:attach has stored a PDF, the result becomes {"pmid": …, "status": "opened", "path": "…/raw/<sha256>/source.pdf"}.

  3. Plan the export with --dry-run

    Choose the papers with any selector. --dry-run resolves the set, checks for duplicates against a read-only snapshot of Papers, and prints the whole plan without writing a file. Claude looks for ~/Library/Application Support/Papers/*.db and passes it as --papers-db.

    output from a scratch library (trimmed)

     /ref:export-papers --project tdcs-review --screened included --batch wm-batch --tags-from tdcs-review --notes --dry-run
    {
      "dry_run": true,
      "destination": "…/lib/exports/papers/wm-batch",
      "manifest": {
        "selector_expression": "--project tdcs-review --screened included",
        "pmids": ["38000101", "38000102"],
        "citekeys": ["rivera2023transcranial", "lindqvist2024working"],
        "destination_reason": "no destination configured; using exports/papers/<batch>/",
        "layout": "flat",
        "pdfs_mode": "copy",
        "per_paper": {
          "38000101": { "citekey": "rivera2023transcranial", "pdf_path": null, "already_known": false, "pushed_note_hash": "a52d955f…" },
          "38000102": { "citekey": "lindqvist2024working", "pdf_path": null, "already_known": true, "pushed_note_hash": null }
        },
        "missing_pdfs": ["38000101", "38000102"],
        "already_known": ["38000102"],
        "skipped_known": [],
        "field_omissions": { "38000101": ["volume", "issue", "pages", "issn"], … },
        "foreign_conflicts": [],
        "duplicate_detection_available": true,
        …
      },
      "note_statuses": { "38000101": "pushed", "38000102": "no notes.md content" }
    }
    OptionWhat it decides
    --to <dir>Destination. Otherwise the default is papers_export_dir from config.json, and failing that exports/papers/<batch>/, which the output reports as destination_reason.
    --layout papers|flatpapers mirrors the app's own <LastAuthor>/<Journal>-<Year>.pdf and adds -2, -3 on collision. It's the default when writing into the configured folder. flat writes <citekey>.pdf and is the default otherwise.
    --pdfs copy|link|noneCopies are the default and leave raw/ untouched. none produces a metadata-only bibliography with no local-url.
    --notes · --tags · --tags-fromPut notes.md into the BibTeX note field and tags into keywords. --tags-from <project> tags members with the project slug.
    --skip-known · --forceLeave out papers the Papers snapshot already holds (matched on PMID, else DOI), or export them anyway.
    --collection <name>Only recorded in the manifest. It never creates a collection in Papers.
    --refreshRe-resolve the selector even if a manifest already exists at the destination.
  4. Export for real

    Drop --dry-run. This run also adds --skip-known, because 38000102 is already in Papers.

    output from a scratch library (trimmed)

     /ref:export-papers --project tdcs-review --screened included --batch wm-batch --tags-from tdcs-review --notes --skip-known
    {
      "dry_run": false,
      "destination": "…/lib/exports/papers/wm-batch",
      "manifest": {
        "pmids": ["38000101"],
        …
        "already_known": ["38000102"],
        "skipped_known": ["38000102"],
        …
      },
      "note_statuses": { "38000101": "pushed" }
    }

    This is the references.bib it wrote, in Papers' own BibTeX dialect. There's no local-url because this scratch paper has no PDF yet:

    @article{rivera2023transcranial,
      title = {{Transcranial stimulation and working memory in adults with ADHD: a randomized trial}},
      abstract = {{We randomized 80 adults...}},
      author = {Rivera, Ana and Okafor, Chidi and Lindqvist, Maja},
      journal = {J Atten Disord},
      year = 2023,
      doi = {10.1000/example.101},
      pmid = {38000101},
      pmcid = {PMC0000101},
      note = {---
    2026-09-15T10:17:36.789825+00:00
    
    Strongest RCT so far; sham blinding reported in supplement.},
      keywords = {tdcs-review}
    }
    Foreign files are never overwritten. The export keeps its own record of every path it wrote (.ref-manager-written.json). A file at a target path that isn't in that record belongs to someone else, usually Papers. It's reported under foreign_conflicts and left alone.
    No Papers database? The export still runs. If the snapshot can't be taken, the manifest shows "duplicate_detection_available": false with the reason, for example "no database at …/missing.db", and the export carries on.
  5. Import in Papers (you)

    In Papers, import references.bib from the destination folder through the app's own import dialog. The plugin doesn't drive the app, and it doesn't assume a watched folder picks the file up. Once imported, check the Notes and Tags columns: the note and the tdcs-review tag should be there.

  6. Re-exporting won't overwrite a note you've edited in Papers

    You may have edited the note inside Papers since the first export, so a second --notes push is refused for any paper whose note has already been pushed. The entry is still exported, just without the note.

    output from a scratch library (trimmed)

     /ref:export-papers 38000101 --batch wm-batch-2 --notes
    {
      …
      "note_statuses": {
        "38000101": "refused: note already pushed for 38000101 (hash a52d955f5b90...); use --notes=force to overwrite"
      }
    }
    The refusal is library-wide. Pushes are tracked per PMID in exports/papers/note_pushes.json, not in each destination's manifest. That's why the example above is refused even though wm-batch-2 is a new destination. --notes=force on the slash command maps to the script's --notes-force flag and overwrites.
    Highlights never travel outbound. BibTeX has no field for them. They're also anchored to a file hash and page rectangles that only make sense against Papers' own copy of the PDF. Annotations move in one direction: from Papers into the library.
  7. Pull your annotations back

    /ref:pull-annotations reads the matching Papers item from a fresh snapshot and upserts annotations.json, keyed by each annotation's own Papers id. notes.md is never touched.

    output from a scratch library

     /ref:pull-annotations 38000102
    {
      "pmid": "38000102",
      "status": "ok",
      "added": ["ann-1", "ann-2"],
      "updated": [],
      "tombstoned": [],
      "unchanged": []
    }
    # again, with nothing changed upstream
     /ref:pull-annotations 38000102
    { "pmid": "38000102", "status": "ok", "added": [], "updated": [], "tombstoned": [], "unchanged": ["ann-1", "ann-2"] }
    # after deleting the margin note ann-2 in Papers
     /ref:pull-annotations 38000102
    { "pmid": "38000102", "status": "ok", "added": [], "updated": [], "tombstoned": ["ann-2"], "unchanged": ["ann-1"] }

    The other statuses don't fail the command either:

    output from a scratch library

    # no Papers database found
    { "pmid": "38000102", "status": "no_papers_db_configured" }
    # this paper isn't in your Papers library
    { "pmid": "38000101", "status": "no_matching_papers_item" }
    # the live database couldn't be read: "status": "snapshot_unavailable", with a "reason"
    Why tombstones. An annotation that disappears upstream might be an accidental delete or a sync hiccup. Locally it gets deleted_upstream: true and a timestamp, and it stays in the file. The tombstoned list makes the loss visible instead of silent.
  8. Full text from Papers: not implemented yet

    PLAN.md §7 describes an opportunistic step: during /ref:fetch, reuse Papers' own extracted full text when the app already holds the paper. The implemented /ref:fetch doesn't do this. It tries PMC OA, then Unpaywall, then publisher HTML, and otherwise records the paper as abstract-only. If a PDF is only available inside Papers, export it from the app and attach it:

    illustrative output

     /ref:attach 38000102 ~/Downloads/lindqvist-2024.pdf
    # attached / duplicate_noop / refused (DOI and title not found on the first two pages) / failed
     /ref:open 38000102
    # now "status": "opened"

What you now have

<LIBRARY>/
  papers/38000101/
    notes.md                         # yours; exported once as the BibTeX note
  papers/38000102/
    annotations.json                 # pulled from Papers; tombstones kept
  exports/papers/
    note_pushes.json                 # per-PMID hash of each pushed note, library-wide
    wm-batch/
      references.bib                 # Papers dialect: note, keywords, local-url when a PDF exists
      manifest.json                  # selector, PMIDs, PDF paths, duplicates, omissions, conflicts
      .ref-manager-written.json      # paths this plugin owns at the destination
      <citekey>.pdf                  # with --pdfs copy|link and an acquired PDF
    wm-batch-2/ …
~/Library/Application Support/Papers/*.db   # read through snapshots only, never written

Command reference: /ref:export-papers, /ref:open, /ref:pull-annotations, /ref:fetch.