arxiv-doc-builder — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited arxiv-doc-builder (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.
Automatically converts arXiv papers into structured Markdown documentation for implementation reference.
This skill automatically:
$...$, $$...$$)convert-paper runs convert_pdf_simple.py (single-column pdfplumber extraction) as a best-effort fallback{ARXIV_ID}/{ARXIV_ID}.md under the output directory (default: current working directory)Invoke this skill when the user requests:
Use the main orchestrator script or the globally installed convert-paper command:
# Using global command (recommended)
convert-paper ARXIV_ID [--output-dir DIR]
# Using script directly
uv run arxiv_doc_builder/convert_paper.py ARXIV_ID [--output-dir DIR]--output-dir: Directory where {ARXIV_ID}/{ARXIV_ID}.md will be created. Default: current working directory (not a papers/ subdirectory).-V / --version: Print the version and exit. Resolves from installeddistribution metadata, falling back to pyproject.toml when run straight from the source tree (the uninstalled case for uv run …/convert_paper.py).
The orchestrator:
fetch_paper.py to download available materials — source if available + PDF (idempotent — cached files are reused)convert_latex.py or convert_pdf_simple.py){output-dir}/{ARXIV_ID}/{ARXIV_ID}.mdAll HTTP requests (curl), file extraction (tar), and directory creation (mkdir) are handled automatically.
Generated Markdown includes:
version, published date, categories, DOI/journal, abstract) — see references/output-format.md; the schema is defined by arxiv_doc_builder/arxiv_metadata.py
$f(x) = x^2$$$\int_0^\infty e^{-x} dx = 1$$Output location: {output-dir}/{ARXIV_ID}/{ARXIV_ID}.md (default output-dir is current working directory)
convert-paper only calls convert_pdf_simple.py as a naive fallback. The other scripts below are for manual or agent-driven use when the naive output is insufficient. Iterate by trying different scripts and inspecting results.
Convert all pages as single-column layout.
uv run arxiv_doc_builder/convert_pdf_simple.py paper.pdf -o output.mdConvert all pages as double-column layout (for academic papers).
uv run arxiv_doc_builder/convert_pdf_double_column.py paper.pdf -o output.mdExtract specific pages with optional double-column processing.
# Extract specific pages
uv run arxiv_doc_builder/convert_pdf_extract.py paper.pdf --pages 1-5,10 -o output.md
# Extract with mixed column layouts
uv run arxiv_doc_builder/convert_pdf_extract.py paper.pdf --pages 1-10 --double-column-pages 3-7 -o output.mdNote: --double-column-pages must be a subset of --pages. Invalid page ranges cause immediate error.
All three scripts share common conversion logic through pdf_converter_lib.py, ensuring consistent behavior while keeping each script focused on its specific use case.
For papers with complex mathematical formulas where text extraction fails, a vision-based approach is available as a manual fallback:
# Generate high-resolution images from PDF
python arxiv_doc_builder/convert_pdf_with_vision.py paper.pdf --dpi 300 --columns 2This creates page images (with optional column splitting) that can be read manually with Claude's vision capabilities for maximum accuracy. This is NOT part of the automatic workflow—use it only when automatic conversion produces poor results.
PDF conversion is inherently lossy:
PDF conversion is acceptable when no LaTeX source is available and the paper is primarily text. For math-heavy papers, use the vision-based approach above or keep the PDF as the primary reference.
Fallback strategy for complex papers:
convert_pdf_simple.pySome arXiv papers (e.g., PRL with supplemental material) contain multiple .tex files, each with its own \documentclass. Automatic selection is unreliable in this case — the canonical example is 1911.04882, which ships both the main PRL paper and an independent PRL supplement, and either can convert successfully. Since pandoc succeeding is not evidence that the selected file is the correct entry point, convert-paper refuses to guess: it fails explicitly with exit code 2 and lists all candidates.
Example failure output:
Error: Found 2 files with \documentclass in /path/to/1911.04882/source:
- /path/to/1911.04882/source/main_paper.tex
- /path/to/1911.04882/source/supplemental_material.tex
Main .tex selection is ambiguous. Re-run with --tex-file pointing at the correct file, e.g.:
convert-paper <ARXIV_ID> --tex-file /path/to/1911.04882/source/main_paper.tex
If you originally passed --output-dir, include the same value in the re-run.To resolve, re-run convert-paper with --tex-file pointing at the correct main file. The fetch step is idempotent, so the already-downloaded source is reused without touching the network:
convert-paper 1911.04882 --tex-file /path/to/1911.04882/source/main_paper.texIf the original run used --output-dir, pass the same value again so that convert-paper reconstructs the correct paper directory.
When pandoc fails on a LaTeX source, the error may point to \end{document} with unexpected \end. This means pandoc's parser broke down due to a syntax issue elsewhere — \end{document} itself is not the cause. Do NOT attempt broad preprocessing (replacing documentclass, expanding \newcommand, removing environments, etc.) — pandoc handles revtex4/revtex4-2, custom commands, picture environments, and theorem environments correctly.
\begin{document} to \end{document}), then test pandoc with increasing prefixes to find the first line that causes failure.{ or } in the LaTeX source. LaTeX's TeX engine silently tolerates these, but pandoc's structured parser does not.{) is usually sufficient. The fetch step is idempotent, so the cached source and PDF are reused without network access.The source (see, e.g., {\cite{makhlin}) has an unmatched {. LaTeX compiles fine but pandoc fails. Fix: remove the stray {.
A brace-mismatch failure is fast — pandoc errors in seconds. A different failure mode is the hang: convert-paper never returns. convert_latex.py bounds pandoc on two axes so this surfaces as a fast error instead of an indefinite hang (both env-overridable):
PANDOC_TIMEOUT_SECONDS, default 180s; ARXIV_PANDOC_TIMEOUT). This is the reliable control — every observed runaway is killed by it.PANDOC_RSS_CAP_MB, default 8192; ARXIV_PANDOC_RSS_CAP_MB). Polls the child's real resident memory and kills early; defense-in-depth for a fast-allocating runaway the timeout alone wouldn't contain in time.Why both, and not the obvious one-liners: observed runaways come in two shapes — a CPU spin at flat memory, and a slow leak (~10 MB/s, reaching tens of GB only after many minutes). A timeout catches both, and for the slow leak it also bounds peak memory (≈ timeout × leak-rate, so ~1.8 GB at 180s). A memory cap alone would miss the CPU-spin shape. The naive memory caps were measured not to work on this failure: GHC's pandoc +RTS -M2g -RTS heap limit did not stop the runaway (major-GC checks don't fire fast enough), and a macOS RLIMIT_AS cap (4/8/16 GB) didn't kill it either (enforcement is unreliable and collides with the GHC RTS reserving a huge virtual address space). Hence the watchdog polls RSS externally (ps -o rss=), which is what actually correlates with swap thrash. You may still hit a hang when driving pandoc manually without these bounds.
Find the pandoc PID (ps aux | rg pandoc) and read its state in one shot:
ps -o pid,etime,time,%cpu,rss,state -p <PID>R (running) and CPU time tracks etime → on-CPU (slow or runaway), not deadlocked..md size is not a progress signal: pandoc buffers the whole document and writes it only at the end (0 bytes until done)..sty filespandoc's only channel from a .sty is the macro table it extracts (there is no per-package special-casing for names like arxiv). arXiv source tarballs commonly bundle a style file (arxiv.sty, conference styles, classicthesis-derived headers) right in the source directory, and pandoc reads any local .sty whose name matches a \usepackage. The blowup is triggered by a self-referential macro redefinition that is then invoked, e.g. the "reduced leading" idiom:
\renewcommand{\normalsize}{\@setfontsize\normalsize\@xpt\@xipt ...}
\normalsize % invoking itTeX is fine (\@setfontsize consumes \normalsize as a non-expanded argument); pandoc does not know \@setfontsize, so on the invocation it re-expands \normalsize inside its own body without bound. Verified minimal repro: self-reference + invocation blows up; the same definition without invocation, or a non-self-referential body, converts instantly.
.sty (safe, and provably output-neutral here)Move the style .sty out of the source directory (reversible) or comment its \usepackage, then re-run — the fetch step is idempotent, so the cached source is reused:
mv source/arxiv.sty source/arxiv.sty.bak # pandoc no longer reads itRemoving a .sty is not a blanket no-op, but the impact is decidable: it changes output only on (commands the .sty defines/redefines) ∩ (commands used in the body). For a style-only package that intersection is layout scaffolding — \section/\subsection/\maketitle (which pandoc renders better from its built-ins; the .sty's \@startsection redefinition actually mangles headings) plus front-matter like \keywords. Prose, math, citations, and glossary terms are untouched. Before stripping, confirm the .sty defines no content macro used in the body (e.g. \newcommand{\co}{ACME}); if it does, that text would be lost and you must instead provide a stub (next section). For style-only packages the intersection contains no content macro, so stripping is output-equivalent on the substantive content.
Symptom: a fast unexpected ( / unexpected [ error, often reported at \begin{document} (the real cause is elsewhere — pandoc parsed to a boundary). Cause: a heavily-used package whose commands take optional arguments pandoc doesn't know the arity of — most commonly glossaries / glossaries-extra (\gls, \glspl, \glsxtrlong, …, including the \gls[prereset]{key} optional-arg form) and cleveref (\cref). pandoc mis-counts the braces it should consume and breaks once enough body follows.
Fix: inject arity-correct `\providecommand` stubs (optional-argument-tolerant) just before \begin{document}. \providecommand only defines them because pandoc never loaded the real package:
\makeatletter
\providecommand{\gls}[2][]{#2}\providecommand{\glspl}[2][]{#2}
\providecommand{\Gls}[2][]{#2}\providecommand{\Glspl}[2][]{#2}
\providecommand{\glsxtrlong}[2][]{#2}\providecommand{\glsxtrlongpl}[2][]{#2}
\providecommand{\glsxtrshort}[2][]{#2}\providecommand{\glsxtrshortpl}[2][]{#2}
\providecommand{\glsentryshort}[1]{#1}\providecommand{\glsentrylong}[1]{#1}
\providecommand{\glslink}[3][]{#3}\providecommand{\glsadd}[2][]{}
\providecommand{\cref}[1]{#1}\providecommand{\Cref}[1]{#1}
\makeatotherQuality note: this expands \gls{AF} to its key (AF), not the glossary long form ("activation function") — pandoc cannot resolve the glossary database. Keys are usually readable (ReLU, NN, tanh), which is acceptable for an implementation-reference doc; \cref{sec:x} likewise renders as the label sec:x, not a link.
This is a targeted exception to the "no broad preprocessing" rule above: stubbing a fixed set of known unknown-arity commands, not rewriting the document.
Output is created under --output-dir (default: current working directory):
{output-dir}/
└── {ARXIV_ID}/
├── source/ # LaTeX source files (if available)
├── pdf/ # PDF file
├── {ARXIV_ID}.md # Generated Markdown output
└── figures/ # Extracted figures (if any)~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.