Markdown Linters Comparison
How mdsmith compares to other Markdown linters.
This document compares mdsmith with popular Markdown linting and formatting tools. It also covers the emerging use of LLMs as linters.
# Tool Overview
# mdsmith
Go binary with zero runtime deps. It ships 69 rules, MDS001 through MDS070 (MDS060 is unused). They cover structure, readability, cross-file links, and generated content.
26 of the 69 rules are opt-in (off by default), among them conciseness-scoring (MDS029 ); the rest run by default.
Key differentiators:
- Token-budget rule (MDS028 ) for LLM context windows
- Paragraph readability (ARI grade) and structure limits
- Regenerable sections: catalog, include, toc, required-structure
- Git merge driver for auto-resolving generated sections
- Metrics subsystem (bytes, lines, words, headings, tokens)
- Offline rule docs compiled into the binary
# markdownlint (markdownlint-cli2 )
Node.js. ~60 built-in rules (MD001 -MD059 ), 31 auto-fixable. The most widely adopted Markdown linter. GitHub uses it internally via markdownlint-github .
- Config: JSONC, YAML, or JS
- Official VS Code extension and GitHub Action
- Custom rules via npm packages
- Mature ecosystem with Prettier compatibility presets
- ~5.9k GitHub stars (library)
# remark-lint
Node.js. ~70 rules distributed as individual npm packages. Part of the unified /remark AST pipeline ecosystem.
- Architecture: parse to mdast AST, lint, serialize
- Three maintained presets (consistent, recommended, style)
- “Fix” by round-tripping through AST (reformat entire file)
- Powers MDX, Gatsby, and Next.js documentation
- Deep composability with unified plugins
- ~8.8k stars (remark parent project)
# rumdl
Rust binary, zero runtime deps. ~1.1k stars. Positions itself as “a modern Markdown linter and formatter, built for speed with Rust” — an explicit, ruff-inspired drop-in replacement for markdownlint.
- 71 lint rules, each mapped to a markdownlint ID (MD001 -style); also reads existing markdownlint JSON/YAML config, so a repo can switch with no rule rewrite
- Autofix via
rumdl check --fixplus arumdl fmtformatter - Config: TOML (
.rumdl.toml, or[tool.rumdl]inpyproject.toml); parent-dir discovery like ESLint/Git - LSP server plus VS Code/Cursor/Windsurf and JetBrains extensions
- Flavor switches for GFM, MkDocs, MDX, and Quarto
- Install: cargo, pip, npm, Homebrew, Nix, mise, winget, binary download
- Benchmarked on the Rust Book repo (478 files, Oct 2025); see Benchmarks
# mado
Rust binary. Tagline: “A fast Markdown linter written in Rust. Compatible with CommonMark and GitHub Flavored Markdown (GFM).” Speed-first: the README leads with a benchmark, not a feature list.
- ~41 rules mapped to markdownlint IDs (MD001-MD047 with gaps); each rule is tagged stable, unstable, or unsupported
- Check-only: no autofix, no
fixmode, no LSP as of 2026-05 - Config: TOML (
mado.toml/.mado.toml) with a published JSON Schema; per-platform global config path - Install: Homebrew, Nix, pacman, Scoop, WinGet, prebuilt binaries
- Ships a GitHub Actions integration
- Headline claim: “≈49-60x faster than existing linters”; numbers in Benchmarks
# panache
Rust binary. Not a markdownlint clone — its own rule IDs and a different target. Tagline: “A language server, formatter, and linter for Markdown, Quarto, and R Markdown, built in Rust with a lossless CST parser and support for external formatters and linters on code blocks.”
- Three tools in one:
panache lint,panache format, and a full LSP (diagnostics, code actions, symbols, folding) - Lossless CST parser keeps Pandoc/Quarto syntax (fenced
divs, attribute spans) instead of flattening it — the
stated edge over Prettier and mdformat on
.qmd/.Rmd - Delegates embedded code blocks to external formatters and linters rather than reimplementing them
- Config: TOML (
panache.toml/.panache.toml) - Install: cargo, prebuilt binaries, AUR, Nix, PyPI (uv/pipx), npm, VS Code extension
- Ships reproducible hyperfine benchmarks against Prettier, Pandoc, rumdl, mdformat, mado, and markdownlint; see Benchmarks
# gomarklint
Go binary, MIT. Tagline: “Catch broken links before your readers do — and keep your Markdown clean while you’re at it.” A young, deliberately small linter (v3.2.x as of 2026-06) with its own kebab-case rule IDs, built for CI gates.
- 23 rules: heading structure, blank-line placement,
fence/emphasis/list-marker consistency, bare URLs, and
link checks. All ship enabled except
external-linkandmax-line-length external-link(opt-in) HTTP-validates external URLs with timeout, retry, and per-host rate-limit settings — the only tool on this page that checks links over the networklink-fragmentsresolves#anchorlinks against a configurable heading-slug algorithm (GitHub style by default)- Check-only: no autofix and no LSP; a VS Code extension is planned. Output is text or JSON
- Config: JSON (
.gomarklint.json); a rule istrue,false,"warning", or an options object; unlisted rules stay enabled - Install: Homebrew tap, npm,
go install, curl script, or GitHub release binaries; ships a GitHub Action and a pre-commit hook
| Aspect | gomarklint | mdsmith |
|---|---|---|
| Distribution | static Go binary | static Go binary |
| Rule set | 23, kebab-case IDs | 69, MDSxxx IDs |
| Autofix | no | mdsmith fix, multi-pass |
| LSP / editor | no (VS Code planned) | LSP for any editor |
| Config | .gomarklint.json | .mdsmith.yml |
| External URLs | external-link (HTTP) | not checked (offline by design) |
| Cross-file | no | links, includes, catalogs, kinds |
| Generated | no | catalog, include, toc, build |
22 of its 23 rules have an mdsmith analog; the
peer-linter coverage matrix
lists each pairing.
The exception is external-link: mdsmith makes no network
calls at runtime (see
Security Posture
), so validating that
an external URL still answers HTTP 200 is deliberately out
of scope. A repo that wants both layers runs gomarklint’s
external-link job next to mdsmith check in CI.
# gomarklint equivalence and the line-scanner trade-off
gomarklint 3.2.3 parses no Markdown AST. It strips front matter, splits the file into lines, and scans each line with hand-written byte and string matching. Its only dependencies are a glob library and a CLI parser — no goldmark, no CommonMark parser. mdsmith parses every file to a goldmark AST. That one architectural difference sets where the 22 shared rules are genuine equivalents and where they are not.
Most gomarklint rules track fenced-code state and strip inline-code spans themselves as they scan. So they match an AST linter on the common cases. Three rules diverge:
max-line-lengthmeasures bytes, not characters, so a line of CJK or emoji over-counts the limit. It exempts only code fences, ATX headings, and whole-line URLs, not tables or reference-link definitions. mdsmith’s MDS001 counts characters and excludes code blocks, tables, and URLs, and runs by default; gomarklint leaves its own rule off. At a shared limit of 30, gomarklint flags a 20-character CJK line withline exceeds 30 bytes (60), which mdsmith passes.duplicate-headingdoes not track code fences, so a#line inside a fenced block counts as a heading. It also reads any#-led line as a heading, with or without a following space. mdsmith resolves headings from the goldmark AST. For a# Buildinside a fence, gomarklint printsduplicate heading: "build"; mdsmith reports nothing.link-fragmentsresolves#anchorlinks within one file only; it has no cross-fileother.md#sectionresolution. mdsmith’s MDS027 walks the whole-repo link and anchor graph, so the shared mapping is a subset of what mdsmith checks, not an equal. On a brokenother.md#nope, gomarklint stays silent while MDS027 reportsbroken link target "other.md#nope" not found. gomarklint does carry a richer per-file slug vocabulary (GitHub, GitLab, Hugo, MkDocs), configurable per project.
The trade runs both ways. gomarklint deliberately skips a
lone URL fenced by blank lines (a GitHub or Zenn
link-preview card) that markdownlint and mdsmith both flag;
mdsmith’s MDS012 reports the standalone URL gomarklint passes
over. Its external-link rule then validates live URLs over
the network, the one check mdsmith omits by design.
Every line of output quoted here comes from gomarklint 3.2.3 and mdsmith on minimal fixtures. The gomarklint equivalence evidence note carries each fixture, the exact commands, and both tools’ full output.
gomarklint claims 100,000+ lines in ~170 ms for its
structural checks. It is pinned into the first-party
benchmark
harness and measured on every
merge. The per-merge assets copy already carries its row;
the committed in-repo tables add it at the next deliberate
refresh. See the gomarklint fairness note in the
benchmark doc
for how its time reads against the
others.
# Prettier
Node.js. Opinionated formatter, not a linter. ~51.7k stars.
- Reformats Markdown with zero config
- Formats embedded code blocks (JS, TS, CSS, JSON, etc.)
- Key option:
proseWrap(preserve, always, never) — controls whether Prettier reflows paragraph line breaks to fit the print width, unwraps paragraphs onto single lines, or leaves existing breaks untouched - No diagnostics, no structural checks, no rule toggles
- Best paired with a linter for structural validation
# Vale
Go binary. Prose and style linter, not a structural linter. Checks writing quality against style guides (Microsoft, Google, AP, custom). ~5.3k stars.
- 11 extension points (existence, substitution, metric, etc.)
- Styles are YAML rule files; no code required
- Config:
.vale.ini(INI format) - Markup-aware: Markdown, HTML, RST, AsciiDoc, DITA, XML
- Used by Grafana, GitLab, DigitalOcean for docs
- Official GitHub Action and VS Code extension
# textlint
Node.js. Zero built-in rules, fully pluggable architecture modeled after ESLint. ~3.1k stars.
- 100+ community rules available via npm
- Parser plugins for Markdown, HTML, RST, AsciiDoc, Typst
- Autofix via
--fixfor rules implementing the fixer API - MCP server support (v14.8+, enhanced in v15.2) for AI assistant integration
- Strong Japanese language support
# Hugo
Go binary. Static site generator, not a linter. ~78k stars. Included here because Hugo’s templating overlaps with mdsmith’s directive system, and teams often weigh the two when deciding where docs automation should live.
- Reads Markdown plus YAML/TOML/JSON front matter and renders to HTML via Go templates
- Shortcodes
(
{{< ... >}}) inject generated content (TOC, file inclusion, catalog-like lists) at build time - No linting or diagnostics — invalid Markdown either renders silently or fails the build
- Output lives in
public/and is typically gitignored; the rendered HTML is the deliverable - Front matter is the canonical metadata source for taxonomies, list pages, and template variables
Hugo and mdsmith differ on where generated content lives:
| Aspect | Hugo | mdsmith |
|---|---|---|
| Generated output | Separate public/ HTML tree | In-place inside the source .md |
| Source readability | Templates obscure final body | Source always renders as-is |
| Validation | None (build succeeds or fails) | Diagnostics + autofix |
| TOC / list-of-files | Shortcodes / list templates | <?toc?> / <?catalog?> |
| File inclusion | {{< readfile >}} shortcode | <?include?> directive |
| Variable syntax | {{ .Title }} (Go template) | {title} (front matter field) |
| Merge conflicts | Re-render at build time | merge-driver install resolves |
| Agent friendliness | Indirect (must run build) | Direct (file is the source) |
To ease migration, mdsmith maps common Hugo template fields to placeholders. See the Hugo migration guide for that mapping.
# Obsidian
Electron app. Markdown note-taking tool with local-first
storage. ~60k stars. Included here because teams that
write docs in Obsidian often want structural linting on
the same .md files.
- Uses its own Obsidian Flavored Markdown
(OFM): wikilinks (
[[Page]]), callouts (blockquote with[!type]prefix), embed syntax (![[file.png]]), and inline metadata (key:: value) - No built-in linter — community plugins (e.g. Linter plugin ) add YAML front matter fixes, heading normalization, and whitespace rules
- Files are plain
.mdon disk and are committed to Git like any other source; CI can run mdsmith over the vault
| Aspect | Obsidian | mdsmith |
|---|---|---|
| Purpose | Note-taking editor | Linter / fixer |
| Linting | Community plugin only | Built-in, CI-ready |
| Wikilinks | Native ([[Page]]) | Validated by MDS027 |
| Callouts | Native (> [!note]) | Validated by MDS067 |
| Front matter | YAML or Dataview inline (key::) | YAML only (inline not recognized) |
| Agent friendliness | Editor-centric, manual saves | Direct file access, no editor needed |
Pin convention: obsidian (see the
conventions reference
) to enable both
checks with one config line. mdsmith also ships an
Obsidian plugin
that
runs the engine as WebAssembly inside the vault, so the
same checks surface as inline diagnostics on desktop and
mobile.
The wikilink check resolves [[Page]] against every
workspace file by stem. Matching is case-insensitive
with a shortest-path tie-break.
The callout check accepts the 12 base Obsidian types
and their aliases out of the box. Dataview inline
fields (key:: value) are still not front matter.
The require/schema directives do not read them.
# obsidian-linter
TypeScript plugin for the Obsidian editor — not a standalone CLI. ~1.9k stars, MIT. Tagline: “This Obsidian plugin formats and styles your notes with a focus on configurability and extensibility.”
- 65 rules in six categories: YAML (14), Headings (5), Footnotes (3), Content (16), Spacing (19), Paste (8)
- Autofix only — runs via the “Lint file” and “Lint all files” commands, an opt-in lint-on-save setting, or auto-applies on paste
- Config in the plugin settings UI, not a checked-in file; rules toggle per vault
- No CLI, no CI gate, no LSP — diagnostics never leave the Obsidian process
| Aspect | obsidian-linter | mdsmith |
|---|---|---|
| Distribution | Obsidian plugin | Static Go binary |
| Scope | Active note only | Whole repo walk |
| CI gate | no | mdsmith check |
| Editor | Obsidian only | LSP for any editor |
| Config | per-vault UI | .mdsmith.yml in repo |
| Autofix model | save / paste / command | mdsmith fix, multi-pass |
| Cross-file | no | links, includes, catalogs, kinds |
| YAML rules | 14 (key sort, alias, …) | MDS020 + CUE schema |
obsidian-linter and mdsmith sit on opposite sides
of the same vault. obsidian-linter runs inside the
editor and rewrites the note a writer is touching.
mdsmith runs in the repo and gates the whole tree
at commit. A team that writes in Obsidian can
layer both: format on save with obsidian-linter,
then enforce the contract across every .md in CI
with mdsmith.
Sixteen mdsmith rules cover an obsidian-linter analog, eighteen of its rules in all. The peer-linter coverage matrix lists each pairing. The rest have no mdsmith analog and are grouped below.
# obsidian-linter rules with no mdsmith equivalent
Three obsidian-linter categories cover ground no peer linter on this page touches.
YAML structure (14 rules). mdsmith validates YAML shape via MDS020 plus a CUE schema. obsidian-linter rewrites YAML content.
yaml-key-sort— sorts the keysyaml-title/yaml-title-alias— derives them from the H1yaml-timestamp— normalises date fieldsformat-yaml-array/sort-yaml-array-values/dedupe-yaml-array-values— normalises arraysmove-tags-to-yaml— promotes inline tags into the front matterformat-tags-in-yaml— normalises tag syntaxescape-yaml-special-characters/force-yaml-escape— escapes string valuesinsert-yaml-attributes— fills missing keysremove-yaml-keys— strips unwanted keysadd-blank-line-after-yaml— blank line after the closing---
Footnotes (3 rules). mdsmith has no footnote rule today.
footnote-after-punctuation— puts the marker after the punctuation, not beforemove-footnotes-to-the-bottom— collects reference definitionsre-index-footnotes— renumbers[^1]markers in document order
On-paste rewrites (8 rules). Applied on paste,
not at lint time. The closest mdsmith concept is
mdsmith fix on save via the LSP.
add-blockquote-indentation-on-pasteprevent-double-checklist-indicator-on-pasteprevent-double-list-item-indicator-on-pasteproper-ellipsis-on-pasteremove-hyphens-on-pasteremove-leading-or-trailing-whitespace-on-pasteremove-leftover-footnotes-from-quote-on-pasteremove-multiple-blank-lines-on-paste
Two Obsidian-specific rules apply only to vault notes:
capitalize-headings— style preferencefile-name-heading— the first H1 must match the filename stem
The rest are prose helpers none of the other linters touch:
auto-correct-common-misspellingsproper-ellipsisandquote-style— straight to typographic formsremove-hyphenated-line-breaks— joins words split by line-ending hyphensremove-multiple-spaces,remove-space-around-characters,remove-space-before-or-after-characterstwo-spaces-between-lines-with-content— forces the Markdown hard-break formspace-between-chinese-japanese-or-korean-and-english-or-numbersconvert-bullet-list-markers,remove-consecutive-list-markers,remove-empty-list-markers,remove-empty-lines-between-list-markers-and-checklistsremove-link-spacing— close to MD039 but applied as autofixcompact-yaml— collapses blank lines inside the front matterconvert-spaces-to-tabs— inverse of MD010empty-line-around-horizontal-rulesempty-line-around-math-blocks,move-math-block-indicators-to-their-own-line
# mdbase
Specification for treating folders of Markdown files as typed, queryable data collections. Reference impl in TypeScript, with a Node CLI and a Rust LSP. MIT, version 0.2.1 (early release as of 2026-05). The same files-on-disk philosophy as mdsmith, but scoped to the data layer: types, queries, and rename refactoring rather than prose linting.
A small example shows the overlap and the split.
Both tools read this .md file as-is:
---
title: Migrate auth to OIDC
status: in-progress
priority: 3
due: 2026-06-01
---
# Migrate auth to OIDC
The current SAML flow has two open issues. We will
swap to OIDC over the next sprint.
See the [migration log](./auth-migration-log.md).What each tool does with the same bytes:
| Layer | mdsmith | mdbase |
|---|---|---|
| YAML front matter | reads it; can validate shape via CUE schema | reads it; validates against _types/task.md |
| Body content (prose, headings) | lints line length, headings, prose, links | not in scope |
| Cross-file link | flags broken auth-migration-log.md (MDS027) | flags broken link (L4) and rewrites it on rename (L5) |
status: in-progress | available to mdsmith list query | filterable in Bases queries; appears in backlink graphs |
due: 2026-06-01 | available to query | filterable with date arithmetic (due <= today() + "7d") |
mdsmith fix runs | reformats tables, regenerates TOC/catalog | n/a |
mdbase rename runs | n/a | moves the file and rewrites every incoming link |
| Body readability, structure, token budget | yes (MDS023 ARI, MDS024 sentences, MDS028 token budget) | no |
The shared layer is the YAML front matter.
Both tools read status, priority, due as
structured fields. mdbase enforces field types
out of the box via _types/. mdsmith does the
same when a CUE schema is wired up via MDS020;
without one, it treats them as plain YAML.
The current surface difference sits in the body and the link graph. mdsmith ships prose, structure, and generated-content rules today. mdbase ships rename refactoring, the link graph, and richer queries today. Either surface is a snapshot, not a charter — see the deep-dive for evolutionary candidates either way.
See the deep-dive comparison . It covers types, queries, validation, links, the fix engine, workflows, and how to run both tools together.
# LLM as Linter
Using language models (GPT-4, Claude, etc.) directly to check prose quality, conciseness, and style. This is emerging through dedicated CLI tools and AI review bots.
How it works:
- Send Markdown to an LLM with a style prompt
- LLM returns diagnostics: verbose paragraphs, unclear phrasing, jargon, redundancy
- Tools wrap this in CLI or CI workflows
Dedicated tools:
- VectorLint
: CLI AI prose linter. Rules defined
in natural language in a
VECTORLINT.mdfile. Uses error-density scoring and 1-4 rubrics. Supports OpenAI, Anthropic, and Google providers. - GPTLint : two-pass LLM linter. A cheap model finds candidates, a strong model filters false positives. Uses GritQL to pre-filter files. Cost: ~$0.83 for 351 API calls on its own codebase (source ).
AI review services:
- GitHub Copilot code review (reviews Markdown in PRs)
- CodeRabbit (combines 40+ linters with LLM review)
- Claude Code Action (Anthropic’s PR review action)
Hybrid systems:
- Grammarly: rule-based grammar + ML models. Their CoEdIT model (770M-11B params) outperforms GPT-3 at text editing while being ~60x smaller (paper ).
Strengths:
- Excels at subjective quality: conciseness, clarity
- Catches semantic issues no rule-based tool can detect
- A single natural-language instruction replaces many regex patterns (e.g. “flag hedging language”)
- Understands context and intent, not just patterns
Weaknesses:
- Non-deterministic: same input may yield different output, even at temperature=0
- Costly: API calls per file per run add up
- Slow: seconds per file vs milliseconds for rules
- Requires network (local LLMs work but lower quality)
- Hard to use as a blocking CI gate reliably
# Feature Comparison
# Structural Linting
All three cover the core structural rules; markdownlint has
the broadest set. The full rule-by-rule mapping lives in the
markdownlint coverage matrix
: every markdownlint
MDxxx, the mdsmith rule that covers it or the plan that
schedules it, and the mdsmith-only rules. As of 2026-05
mdsmith implements all 52 active markdownlint rules (49
fully, 3 partial).
# Rust Markdown linters (rumdl, mado, panache)
Three Rust tools sit next to mdsmith. rumdl
and mado
are markdownlint-compatible: they adopt markdownlint’s
MDxxx rule IDs as a drop-in surface. panache
is not —
it keeps its own IDs and targets Pandoc/Quarto/R Markdown.
mdsmith also keeps its own MDSxxx IDs and adds a
cross-file, generated-content, and readability layer none
of the three carry.
In the coverage matrix
the markdownlint column
doubles as the rumdl/mado column: both reuse the same
MDxxx semantics. rumdl implements ~71 of those IDs; mado
implements ~41. Neither adds rules outside the markdownlint
set. panache does not map to that matrix — its checks
target Quarto and
R Markdown constructs the others flatten away.
| Aspect | mdsmith | rumdl | mado | panache |
|---|---|---|---|---|
| Language | Go | Rust | Rust | Rust |
| Rule IDs | own MDSxxx | markdownlint MDxxx | markdownlint MDxxx | own |
| Rule count | 69 | 71 | ~41 | unenumerated |
| Autofix / format | fix | --fix, fmt | no | format |
| LSP / editor | yes (LSP) | yes (LSP) | no | yes (LSP) |
| Config format | YAML | TOML | TOML | TOML |
| Reuse markdownlint cfg | no | yes | no | no |
| Cross-file integrity | yes | no | no | no |
| Generated sections | yes | no | no | no |
| Readability/token rules | yes | no | no | no |
| Front-matter schema | yes | no | no | no |
| Quarto / R Markdown | no | Quarto flavor | no | yes (CST) |
# Prose and Readability
| Capability | mdsmith | Vale | LLM |
|---|---|---|---|
| Readability grade | MDS023 (ARI) | metric ext | yes |
| Sentence limits | MDS024 | occurrence ext | yes |
| Word choice | no | substitution ext | yes |
| Passive voice | no | existence ext | yes |
| Jargon detection | no | existence ext | yes |
| Conciseness | MDS029 (experimental) | no | yes |
| Tone enforcement | no | custom styles | yes |
| Token budget | MDS028 | no | no |
| Deterministic | yes | yes | no |
mdsmith focuses on measurable readability metrics (ARI grade, sentence count, token budget). Vale excels at style guide enforcement. LLMs handle subjective quality best but lack determinism.
# Formatting and Fixing
| Capability | mdsmith | Prettier | markdownlint | rumdl | mado | panache | remark-lint |
|---|---|---|---|---|---|---|---|
| Autofix CLI | fix | --write | --fix | --fix / fmt | no | format | yes (AST rewrite) |
| Table alignment | MDS025 | yes | no | MD055/56/58 | no | yes | via plugin |
| Prose wrapping | opt-in (reflow) | proseWrap | no | no | no | no | no |
| Embedded code fmt | no | JS/TS/CSS/JSON | no | no | no | delegates to external | no |
| Multi-pass fix | yes | single pass | single pass | single pass | no | single pass | single pass |
| Generated sections | catalog, include, toc | no | no | no | no | no | no |
Prose wrapping controls whether a tool reflows paragraph
line breaks. Only Prettier ships an explicit
prose-wrapping mode: proseWrap
takes
always (wrap to print width), never (unwrap to one
line per paragraph), or preserve (leave as-is, the
default). remark-lint has no prose-wrap setting, but it
serializes through its AST when fixing, so paragraphs
can be incidentally rewrapped to match its stringify
defaults. mdsmith adds an opt-in reflow fix
(line-length.reflow). It rewraps prose to the width
you set, and leaves breaks alone otherwise.
markdownlint, rumdl, mado, and panache flag long
lines but keep the existing breaks.
Prettier is the strongest pure formatter. rumdl and panache bring native autofix to the Rust side; mado is check-only. remark-lint reformats by round-tripping the whole file through its AST. mdsmith is the only tool here with autofix for generated content (catalog, include, toc) and a multi-pass fix loop.
# Cross-File and Project Features
| Capability | mdsmith | markdownlint | remark-lint | rumdl | mado | panache |
|---|---|---|---|---|---|---|
| Link integrity | MDS027 | no | remark-validate-links | no | no | no |
| Include sections | MDS021 | no | no | no | no | no |
| Catalog generation | MDS019 | no | no | no | no | no |
| Required structure | MDS020 | no | no | no | no | no |
| Front-matter query | mdsmith list query | no | no | no | no | no |
| Git merge driver | yes | no | no | no | no | no |
| Metrics ranking | yes | no | no | no | no | no |
| Gitignore aware | yes | yes | no | yes | yes | yes |
| Front matter support | yes | via plugin | via remark-frontmatter | yes | no | yes |
mdsmith has the strongest cross-file and project-level
features. None of the Rust linters cross file boundaries:
they lint each file in isolation. The merge driver and
regenerable sections are unique to mdsmith. The list query subcommand selects files by a CUE expression over
front matter (e.g.
mdsmith list query 'status: "✅"' plan/), which no other
tool in this comparison offers natively.
# Renderer Portability
Several Markdown renderers expand non-standard
tokens into tables of contents. Common
variants are [TOC] (Python-Markdown),
[[_TOC_]] (GitLab, Azure DevOps), [[toc]]
(markdown-it, VitePress), and ${toc} (some
VitePress configs). CommonMark and goldmark —
the engine mdsmith uses — expand none of
them. They render as literal text.
MDS035
(toc-directive, opt-in) flags
each of the four tokens on its own line. For
[TOC], the rule suppresses the diagnostic
when a matching link reference definition
makes it a legitimate link. No other linter
in this comparison detects these tokens.
mdsmith fix replaces each token with a
<?toc?>...<?/toc?> block (MDS038
).
A second fix pass populates the block with a
nested heading list.
# Runtime and Integration
| Property | mdsmith | markdownlint | remark-lint | Prettier | Vale | textlint | LLM |
|---|---|---|---|---|---|---|---|
| Language | Go | Node.js | Node.js | Node.js | Go | Node.js | API |
| Runtime deps | none | Node 20+ | Node 16+ | Node 16+ | none | Node 20+ | network |
| Install | binary | npm | npm | npm | binary | npm | varies |
| Config format | YAML | JSONC/YAML/JS | JSON/YAML/JS | JSON/YAML/JS | INI+YAML | JSON/YAML/JS | prompt |
| Output formats | text, JSON | text, JSON | text | none | text, JSON | text, JSON | text |
| VS Code | yes (LSP) | yes | yes | yes | yes | yes | varies |
| GitHub Action | no | yes | via npm | via npm | yes | via npm | custom |
| Pre-commit | lefthook | husky/lefthook | husky | husky | hooks | husky | impractical |
| Offline | yes | yes | yes | yes | yes | yes | no |
| Deterministic | yes | yes | yes | yes | yes | yes | no |
Go-based tools (mdsmith, Vale) have zero runtime dependencies. Node.js tools require a runtime but benefit from the npm ecosystem. LLM-based linting requires network access and is non-deterministic.
# Benchmarks
Default mdsmith is more than an order of magnitude faster
than markdownlint-cli2 on its own Markdown. With the
mdsmith-only rules disabled (mdsmith-parity), it runs at
mado’s speed: the two tie on the repo corpus, and parity
trails mado only narrowly on the longer neutral corpus. See
the benchmark doc
for the current ratios.
Among the Rust linters, mado leads the per-file race. Default mdsmith does more per file: it walks the cross-file graph, scores readability, and validates generated sections. Even so, it comes in ahead of rumdl and panache on both corpora.
Pick mado for raw markdownlint throughput. Pick panache for Quarto or R Markdown. Pick mdsmith for the cross-file and self-maintaining-section layer.
See the benchmark doc for the full method, both corpora, every tool’s command, the result tables, and the fairness notes.
# When to Use What
mdsmith fits best when you need readability limits, token budgets, or generated content sections. Its single binary makes CI setup simple.
markdownlint is the safe default for teams already in the Node.js ecosystem. Widest community adoption, most editor integrations, battle-tested rule set.
remark-lint suits projects deep in the unified/remark ecosystem (MDX, Gatsby, Next.js). Its AST pipeline enables custom transformations beyond linting.
Prettier is a formatter, not a linter. Use it alongside a linter. Pair with markdownlint (using the Prettier compat preset) or remark-lint for structural checks.
Vale is the right choice for enforcing prose style guides (Microsoft, Google, AP). It complements structural linters rather than replacing them.
textlint works well for polyglot text linting (especially Japanese) and teams wanting ESLint-style modularity.
rumdl is the pick when you want markdownlint’s exact rules and config. You get them as one fast Rust binary, with autofix and an LSP. It is a drop-in speed upgrade for a Node markdownlint setup.
mado fits a check-only CI gate. It just needs markdownlint rules run as fast as it can. There is no autofix, LSP, or front-matter support, and the gate does not need them.
panache is the right choice for Quarto and R Markdown. Its lossless CST keeps the Pandoc syntax that Prettier and mdformat flatten. It bundles the formatter, linter, and LSP for those formats.
gomarklint fits a minimal CI gate that must also catch
dead external links. Its small rule set is check-only, so
fixes stay manual, but the opt-in external-link rule
HTTP-validates every URL — the one check no other tool
here performs. Pair it with mdsmith when the same repo
also needs autofix, cross-file integrity, or generated
sections.
obsidian-linter fits a team that writes notes in Obsidian and wants every save to clean up YAML keys, heading case, and spacing inside the editor. There is no CLI or CI gate, so pair it with mdsmith when the same vault is also a Git repo that needs a commit-time check.
LLM as linter is best for subjective quality checks: conciseness, clarity, tone. Use it in PR review workflows where latency and cost are acceptable. Pair with a deterministic linter for structural rules.
# Combining Tools
Most teams benefit from layering tools. Common pairings:
- Structure + format: markdownlint + Prettier
- Structure + prose: mdsmith + Vale
- Structure + AI review: mdsmith + LLM review in CI
- Full stack: mdsmith (structure + readability) + Vale (style) + Prettier (formatting)
mdsmith’s conciseness-scoring rule (MDS029 ) is an off-by-default, experimental heuristic based on static signals. It catches structural verbosity. Semantic issues still require an LLM.
# Front Matter and Document Templates
Front matter (YAML between --- delimiters) is a key
integration point. Tools handle it differently.
mdsmith uses front matter in three rules:
- catalog (MDS019
): reads front matter
fields from matched files to build summary tables.
Fields become template variables (
{title},{status}). - required-structure (MDS020 ): validates document headings and front matter against a template. Supports CUE schemas for field types and constraints.
- include (MDS021
): strips front matter from
included files by default (
strip-frontmatter: true).
mdsmith also provides proto files as templates for rule and metric docs. The proto defines required front matter fields (id, name, status, description) with CUE validation patterns, required heading structure, and content guidelines. Every rule README is validated against its proto via the required-structure rule.
MDS020 validates front matter fields against CUE schemas embedded in templates. There is no standalone rule that validates front matter without also checking heading structure.
markdownlint has no built-in front matter awareness. It strips front matter to avoid false positives but does not inspect its content. Custom rules can access it.
remark-lint supports front matter via the remark-frontmatter plugin. Rules can then inspect the parsed YAML. No built-in validation rules exist.
Prettier preserves front matter blocks but does not format or validate their content.
Vale is front-matter-aware: it skips YAML blocks to avoid false positives on metadata fields.
# Progressive Disclosure
mdsmith’s catalog rule (MDS019
) implements
progressive disclosure for documentation sets. A summary
table gives readers the overview; each row links to the
full document for details. Running mdsmith fix keeps the
table in sync with source front matter.
This pattern is useful for large repos where readers need to find the right document without reading everything. No other linter in this comparison generates or maintains navigational tables from document metadata.
# Markdown Include / Preprocessor Tools
Several tools provide file inclusion for Markdown. All are preprocessors: they transform source files at build time, producing a separate output file.
| Tool | Language | Include syntax | Stars |
|---|---|---|---|
| markdown-include | Python | {!filename!} | ~100 |
| MarkdownPP | Python | !INCLUDE "file.md" | ~350 |
| Markedpp | Node.js | !include(file.md) | ~50 |
| MyST Markdown | Python | {include} directive | ~400 |
| Gitdown | Node.js | <<< file.md | ~460 |
| mdpre | Python | preprocessor directives | ~20 |
Key differences from mdsmith’s include rule (MDS021 ):
- Build step required. Preprocessors read source files and write transformed output. The source and output are different files. mdsmith regenerates included content in place — the source file is always valid Markdown.
- No validation. Preprocessors replace directives with
file contents but do not lint the result. mdsmith’s
include rule validates that included sections stay in
sync and auto-fixes drift via
mdsmith fix. - Not agent-friendly. Agents read and write the same file. A preprocessor build step adds friction: the agent must know to run the preprocessor after editing, and the included content is invisible in the source. With mdsmith, the included content lives in the source file and is always readable.
# Slidev and Presentation Markdown
Slidev uses Markdown files as slide decks. Slides are
split by --- lines, with YAML front matter for config.
No tool in this comparison has Slidev support:
- Linters treat
---separators as horizontal rules, which may trigger false positives - Front matter blocks between slides may confuse parsers that expect a single front matter block at file start
- Slidev-specific directives (layout, clicks, transitions) appear as YAML or HTML comments that linters ignore
Teams using Slidev alongside standard Markdown docs should use separate config overrides (e.g. ignore or relaxed rules) for presentation files.
# Security Posture
A Markdown linter parses untrusted input from any contributor. It also walks the repo file tree. Adversarial input has caused OOMs, YAML billion-laughs expansion, ANSI escape injection, symlink escapes, and path traversal in the wider linter ecosystem.
mdsmith ran a 10-finding adversarial review . It covered the parser, front-matter loader, terminal output, file walker, include directive, and CUE schemas. All findings were fixed. The current posture:
| Hardening | mdsmith | markdownlint | remark-lint | Prettier | Vale |
|---|---|---|---|---|---|
| File-size cap on input | yes | no | no | no | no |
| YAML billion-laughs guard | yes (alias rejection) | n/a (no FM) | parser-dependent | parser-dependent | n/a |
| ANSI escape sanitization | yes | no | no | n/a | no |
| Symlinks denied by default | yes | follows | follows | follows | follows |
| Cross-file links sandboxed to repo | yes (MDS027 ) | n/a | plugin-dependent | n/a | n/a |
| Include size cap | yes | n/a | n/a | n/a | n/a |
| Schema/CUE path validation | yes (MDS020 ) | n/a | n/a | n/a | n/a |
| Atomic writes in fix mode | yes | n/a | n/a | yes | n/a |
| Network access at runtime | none | none | none | none | none |
| Dependency surface | Go stdlib + goldmark | Node + npm tree | Node + npm tree | Node + npm tree | Go stdlib |
Two structural properties reduce the attack surface relative to Node.js linters:
- Single static binary. No runtime package
resolution, no
node_modulesto audit. Supply-chain risk is the Go module graph ingo.mod, reviewed at upgrade time. - No network calls. Rule docs, schemas, and tokenizers ship inside the binary. CI does not need outbound network for linting, and adversarial Markdown cannot exfiltrate via SSRF.
Teams handling untrusted Markdown (PRs from external contributors, user-submitted content) should treat the linter as a parser of untrusted input. mdsmith aims to fail safely on adversarial input rather than crash or escape the repo root.
# Versioning
The <?build?> directive and its recipe-safety rule
(MDS040
) ship today. Pin a version
(go install github.com/jeduden/mdsmith/cmd/mdsmith@vX.Y.Z)
if you need a stable rule set across upgrades.