anydoc Hit 20,288 Stars in a Month and One Person Wrote 118 of Its 130 Commits
A Rust document converter went from empty repo to 20,288 stars in 32 days and 745,467 npm installs. We converted six formats with it and clocked a 2.90 ms docx against LibreOffice's 1,324 ms, then found where the 5 ms claim falls apart.
The WJS Desk
Sep 5, 2026 · 6 min read

The repo firecrawl/anydoc was created on 3 August 2026. When we pulled the GitHub API on 4 September it had 20,288 stars, 1,228 forks and 87 open issues. That is 32 days.
Star counts lie constantly, so we checked the number that is harder to inflate. npm reports 745,467 downloads of @firecrawl/anydoc since 3 August, including 265,375 in the week ending 29 August and a single-day peak of 48,052 on 25 August. People are not just starring it, they are installing it in CI. Then we installed it ourselves and ran six formats through it.
What it actually is
One job: take an office document, emit GitHub-Flavored Markdown. Pure Rust core, with Node, Python and WebAssembly bindings. No ML models and no network calls in the default path, which is the thing that separates it from the hosted document-parsing services.
The CLI is one document per invocation, stdout by default:
npm install -g @firecrawl/anydoc
anydoc report.docx # markdown to stdout
anydoc report.docx -o out.md # or to a file
anydoc - --format csv < data.csv
The Node API is two functions, and the naming matters more than it looks:
import { toMarkdown, toMarkdownBytes } from '@firecrawl/anydoc'
await toMarkdown('./report.docx') // takes a path
await toMarkdownBytes(new Uint8Array(buf)) // takes bytes
We hit this in the first five minutes. Passing a Node Buffer to toMarkdown throws, because it wants a path. The napi error handler then serialises the entire buffer into the message, so a 37 KB docx produced a 64 KB error dump of decimal byte values in our terminal. It is the correct error for the wrong reason, and if you log errors to a hosted service, one bad call ships the whole document into your logs.
We converted six formats and checked the output against known input
Rather than trusting a README, we built documents whose structure we already knew: a docx with one h1, two h2s, one h3, bold and italic runs, bulleted and numbered lists, and a 3x3 table. Plus a two-sheet xlsx, the public ICANN letter as a real-world PDF, and Project Gutenberg's Alice's Adventures in Wonderland as an EPUB.
The docx came back essentially perfect. Headings at the right levels, **bold** and *italic* correct, and the table rendered as a proper GFM pipe table with the alignment row. We then exported the same file to legacy .doc (the OLE binary format) and to .odt through LibreOffice, and anydoc produced byte-identical Markdown from all three. Reading 1990s OLE streams as cleanly as modern OOXML is genuinely hard and it just worked.
Two honest corrections to findings we nearly published:
Nested lists are fine. Our first test showed a nested bullet flattening into a sibling. That was our test file's fault: we had set a List Bullet 2 style name without the w:ilvl numbering level Word actually writes. When we built the file properly, anydoc preserved depths 0, 1 and 2 with correct indentation. LibreOffice, converting the same file, relabelled the nested bullets as numbered lists.
Formulas are fine, with a trap. Our xlsx showed a =SUM(B2:B3) cell converting to an empty table cell. Before blaming anydoc we injected a cached result into the sheet XML, which is what Excel itself stores alongside the formula. anydoc then emitted 2400 correctly. So it reads cached values and does not evaluate formulas, which is reasonable. The trap is that openpyxl, and any tool that writes a formula without a cached value, produces a file where those cells silently vanish. No warning, no non-zero exit.
The numbers, including the one that fails
The README claims: "Median conversion time is under 5ms per document," and a benchmark table putting anydoc at 4.4 ms median against LibreOffice's 1129.5 ms across 100 documents. We measured in-process with toMarkdownBytes, 40 iterations after 3 warmups, on an Apple Silicon Mac.
| Input | anydoc median | Alternative | Verdict |
|---|---|---|---|
| metrics.xlsx, 2 sheets | 0.11 ms | n/a | Under the claim by 45x |
| report.docx, 37 KB | 2.90 ms | LibreOffice 1,324 ms | Claim holds |
| alice.epub, 137 KB | 4.69 ms | n/a | Just inside the claim |
| icann.pdf, 3 pages | 100.59 ms | pdftotext 47 ms | 20x over the claim, and slower |
So the office formats beat the claim comfortably and the PDF does not come close. Our LibreOffice figure of 1,324 ms for the same docx is in the same ballpark as their 1,129.5 ms, so their comparison is credible. But note what dominates it: LibreOffice's number is mostly process startup, and anydoc's CLI pays its own 40-odd ms of Node startup too. Via the CLI the same docx took 50 ms wall clock, not 2.90 ms. The 400x figure is real for a long-running process and not for a shell script in a loop.
The PDF result is the one to plan around. Their 4.4 ms median is across a mixed corpus of fourteen formats. If your workload is PDF-heavy, your median will not look like theirs. Ours was 100.59 ms on a three-page text PDF, and pdftotext beat it outright, though pdftotext gives you flat text while anydoc gives you structure.
What it does not do
- No OCR. We built an image-only PDF with no text layer. anydoc exited 3 with
anydoc: page 1 of 1 needs OCR, exactly as documented. The--ocr hostedflag sends the document to Firecrawl's Parse API, which is a network call and needs an API key. Ours was refused without one. - No encrypted or password-protected documents.
- No formula evaluation. Cached values only, as above.
- RTF loses heading structure. Round-tripping our docx through LibreOffice's RTF export, the h1 arrived as
**bold text**rather than#. We cannot cleanly attribute this: LibreOffice's RTF writer may not be emitting heading styles at all. If RTF headings matter to you, test with files from your actual source. - Images are alt text only. Markdown cannot embed bytes, so embedded images render as alt text with the raw bytes left on the document model.
The error messages are the best thing here. Feeding it 400 random bytes named .docx: malformed document: not a readable zip archive: invalid Zip archive: Could not find EOCD. Declaring a docx as --format pdf: not a PDF: file appears to be a ZIP archive (possibly an Office document). Exit codes matched the docs every time: 1 for unreadable, 2 for usage, 3 for needs-OCR. That is a tool you can put in a pipeline.
Who made it, and does that matter
Here is the part the star count hides. The GitHub contributors API returns five people, and the distribution is:
| Contributor | Commits |
|---|---|
| tomsideguide | 118 |
| nickscamara | 6 |
| abimaelmartell | 4 |
| Andsu-dev | 1 |
| ericciarla | 1 |
That is a bus factor of one on a project with 745,467 downloads. Ten releases shipped between 5 August and 27 August, then nothing: last push 28 August, a week before we looked.
The mitigating detail is the shape of the code. We cloned it: 27,234 lines of Rust and eight direct dependencies in Cargo.toml (cfb, csv, flate2, encoding_rs, log, pdf-inspector, quick-xml, zip). MIT licensed. There is also a fuzz directory, which for a parser eating untrusted binary formats is the correct instinct.
27,234 lines is too much to casually vendor into your app, but it is a forkable size for a team that depends on it, and eight dependencies means the supply chain underneath is small. Compare that to standing up a LibreOffice container.
Verdict
Adopt today if you are converting office documents to Markdown for a RAG index or an agent and you currently shell out to LibreOffice. The speed difference in a long-running process is not marginal, the format coverage is wider than pandoc or markitdown, and legacy .doc and .odt support is unusually good.
Wait if your corpus is mostly PDFs, where it was both slower than pdftotext and 20x over its own claim, or if scanned documents are a meaningful share, because the only OCR route ships your files to a hosted API.
What would change our mind: a second maintainer with real commit volume. One person wrote 118 of the 130 commits behind three quarters of a million monthly installs. Pin your version, test against it, and do not track main.
One thing we could not explain. Two Hacker News submissions of anydoc, on 5 and 11 August, drew 4 points and 3 points between them. Twenty thousand stars did not come from there. The npm numbers say the usage is real, so we are recording the oddity rather than a theory about it.


