rumdl: a Rust Markdown linter with markdownlint-compatible config
Fast Markdown linter and formatter written in Rust
At a glance
- What is it?
- rumdl is a single-binary Markdown checker and formatter that reads existing markdownlint configuration. The interesting question is not speed, it is whether the rule behaviour matches closely enough that you can swap the binary without rewriting your config.
- Who is it for?
- Adopt rumdl if you already run markdownlint or markdownlint-cli2 in CI and want a single static binary with no Node runtime, and if you are willing to keep your existing config for the first run rather than rewriting it. Do not adopt it if your rule set depends on a markdownlint plugin ecosystem, or if you need a formatting contract that fails the build on every unfixable violation, because rumdl fmt exits 0 even when violations remain.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The problem rumdl targets: Markdown linting that does not require a Node toolchain
Markdown linting in most repositories arrives through markdownlint or markdownlint-cli2, which means a Node runtime, a package.json entry, and a lockfile that has to stay current. rumdl's stated goal is to replace that with one native binary that does linting, formatting, caching, and editor integration. The README describes it as inspired by Ruff, and the comparison is structural rather than cosmetic: Ruff collapsed a set of Python tools into one Rust binary, and rumdl is attempting the same collapse for the Markdown toolchain.
The audience is narrow and identifiable. It is maintainers of documentation-heavy repositories who already have a markdownlint config and a CI job that runs it. The README points at existing configuration discovery as the migration path: keep the markdownlint or markdownlint-cli2 config file for the first run and compare results before changing anything. That framing is honest about where the friction is. Nobody wants to re-tune 84 rules by hand, so the tool's usefulness depends almost entirely on how faithfully it reads the config you already have.
How rumdl is structured: rules, flavors, and a cache
The README lists 84 lint rules and a set of built-in Markdown flavors covering GFM, MkDocs, MDX, Quarto, and MyST, with auto-detection for supported file names. Flavors matter more than they sound. MDX and Quarto embed non-Markdown syntax inside Markdown files, so a linter that treats every file as CommonMark will report violations on constructs that are legal in that dialect. Declaring a flavor tells the parser which constructs to expect. The flavor list also appears under a docs/flavors.md path and a rumdl.dev/flavors page, so flavor selection is a first-class configuration concern rather than a flag.
Caching is the second structural piece. The README states that with caching, subsequent runs only re-lint files that have changed, and names watch mode and editor integration as the beneficiaries. That is the mechanism that makes an editor integration tolerable: an LSP server that re-lints the entire tree on every keystroke is unusable, and one that re-lints only the changed file is not. The README also lists a server command and a vscode command, so the LSP path is shipped rather than planned.
What the material does not describe is the rule engine itself. There is no explanation of how rules are evaluated, whether they run in one pass over an AST or as independent passes, or how fix conflicts between rules are resolved. If you need to know whether rule A's autofix can invalidate rule B's output in the same run, the README does not say.
Installing rumdl and running the first check
The README gives a zero-install path first: uvx rumdl check . runs it once without installing. After that, installation is available through Homebrew, Cargo, npm, pip, uv, mise, Nix, the Termux User Repository, pacman on Arch, or a downloaded binary. The PyPI and crates.io badges confirm both publishing channels exist.
The core commands are short. rumdl check . lints a tree. rumdl fmt . formats files. rumdl check --fix . applies autofixes and reports whatever is left. rumdl init creates a default configuration file. There is also rumdl import <FILE> for bringing in an existing config, rumdl rule for inspecting rules, rumdl config for printing the effective configuration, and rumdl config --defaults and rumdl config --no-defaults to narrow that output to defaults only or non-defaults only.
The exit codes are the part to read carefully, because they are asymmetric. The README states that rumdl fmt exits 0 on success even if unfixable violations remain. rumdl check --fix exits 0 if everything was fixed and 1 if violations remain. Plain rumdl check is described as CI/CD friendly with a non-zero exit code on errors. So the formatting command is not a gate and the check command is. If you wire rumdl fmt . into CI expecting it to fail the build on unfixable problems, it will not.
Configuration discovery and the markdownlint migration path
Configuration is TOML-based, and the README also documents configuration inside pyproject.toml, which is the pragmatic choice for Python repositories that already keep tool config there. Discovery is automatic: rumdl finds common markdownlint and markdownlint-cli2 configuration files on its own. That is the single most consequential design decision in the project, because it means the migration cost is measured in diffed output rather than in config translation.
Inline configuration exists too. The README's own source contains a rumdl-disable-next-line comment, which shows the suppression syntax in use on a real file. There are also style guide presets and an editor JSON schema, so editors can complete config keys.
The honest limitation here is that config compatibility is not the same as rule compatibility. A discovered markdownlint config tells rumdl which rules to enable and how to parameterize them. It does not guarantee that a rule with the same identifier produces identical diagnostics on the same input. The README's own advice, to compare results before changing your workflow, is an admission that the outputs can differ. Treat the first run as a differential test, not a drop-in replacement.
Where rumdl is the wrong tool
The clearest failure mode is the exit-code asymmetry already described. A pipeline that runs rumdl fmt . and treats a zero exit as proof that the tree is clean will pass builds with unfixable violations still present. The check command is the gate; fmt is a convenience.
The second boundary is ecosystem depth. markdownlint has a plugin surface, and the README does not claim to run markdownlint plugins. It claims configuration compatibility and a built-in rule set. If your linting depends on a custom rule package rather than on configuration of the built-in rules, discovery will not help you, and there is no documented plugin mechanism to port it to.
The third is flavor coverage as a correctness question rather than a feature list. The README names GFM, MkDocs, MDX, Quarto, and MyST as supported flavors with auto-detection for supported file names. It does not document what happens on a file whose flavor is not auto-detected and not configured. If your repository mixes dialects under file names the detector does not recognize, you are relying on manual flavor configuration, and the README does not spell out the fallback.
Finally, the performance claim. The README links a benchmark methodology page and a cold-start comparison chart, and describes the results as dated Rust Book measurements with stated scope and limitations. That is a more careful framing than most projects offer, but it is still a single-corpus benchmark. There is no published figure for a repository with thousands of Markdown files, which is the case where caching would matter most.
markdownlint versus rumdl: the actual difference in approach
markdownlint is a Node library with a CLI wrapper, and markdownlint-cli2 is the configuration-and-globbing layer on top of it. Its rules are JavaScript, which is what makes the plugin ecosystem possible: a rule is a module, and adding one means adding a dependency. The cost is the runtime. Every CI runner and every contributor machine needs Node and a resolved dependency tree before a single file is checked.
rumdl inverts that. Rules are compiled into the binary, the binary has no runtime dependencies, and configuration is read from files markdownlint already understands. The trade is explicit: you get deployment simplicity and you give up extensibility. There is no documented way to write a custom rule in rumdl, and no documented plugin loader. If your organization has one bespoke rule about internal link conventions, markdownlint can host it and rumdl, based on the material available, cannot.
The second difference is scope. markdownlint lints. rumdl lints, formats with --fix, caches results, and ships an LSP server and a VS Code command. Whether that consolidation is better depends on whether you currently assemble those pieces from separate tools. If you already run markdownlint plus prettier plus a separate LSP, rumdl replaces three moving parts with one. If you only lint, you are trading a familiar runtime for a smaller one.
Maintenance, releases, and the MIT licence
The release cadence visible in the material is fast: v0.2.70, v0.2.69, and v0.2.68 landed on 2026-09-07, 2026-09-08, and 2026-09-10 respectively, roughly one release every day or two across that window. The version prefix is still 0.2.x, which conventionally signals that the interface is not yet frozen. For a linter, the practical consequence is that rule behaviour and CLI flags can shift between patch releases, so a pinned version in CI is worth more here than it would be for a 1.x tool. Upgrading means re-reading the release notes for rule changes, because a rule that gains a new diagnostic will surface as new violations in your tree rather than as a tool error.
The licence is MIT, per the README badge and the repository metadata. MIT is permissive: it allows use, modification, and redistribution with the licence text preserved, and it carries no copyleft obligation on your own code. That matters for a tool that runs inside CI on proprietary repositories, where a copyleft linter would raise questions. This is a description of the licence text, not legal advice; if your organization has specific policy on bundled dependencies, check the transitive crate licences yourself, because the README does not enumerate them.
The distribution surface is broad, which cuts both ways. Homebrew, Cargo, npm, pip, uv, mise, Nix, TUR, pacman, and direct binary downloads mean almost any environment has a path. It also means the release process has to keep ten channels in sync, and a lagging channel is a plausible failure mode that the README does not address.
Editorial conclusion
Adopt rumdl if you already run markdownlint or markdownlint-cli2 in CI and want a single static binary with no Node runtime, and if you are willing to keep your existing config for the first run rather than rewriting it. Do not adopt it if your rule set depends on a markdownlint plugin ecosystem, or if you need a formatting contract that fails the build on every unfixable violation, because rumdl fmt exits 0 even when violations remain. Before switching, run rumdl check . against your current tree with the old config still in place and diff the reported violations against your existing linter's output.
Community notes