--- summary: How mdsmith compares to other Markdown linters. --- # Markdown Linters Comparison 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][mds001] through [MDS070][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][mds029]); the rest run by default. Key differentiators: - Token-budget rule ([MDS028][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][md001]-[MD059][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][mdl-vscode] and [GitHub Action][mdl-action] - Custom rules via npm packages - Mature tooling 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. - 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][md001]-style); also reads existing markdownlint JSON/YAML config, so a repo can switch with no rule rewrite - Autofix via `rumdl check --fix` plus a `rumdl fmt` formatter - Config: TOML (`.rumdl.toml`, or `[tool.rumdl]` in `pyproject.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](#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 `fix` mode, 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](#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](#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-link` and `max-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 network - `link-fragments` resolves `#anchor` links 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 is `true`, `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][mdcov] lists each pairing. The exception is `external-link`: mdsmith makes no network calls at runtime (see [Security Posture](#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-length` measures 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][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 with `line exceeds 30 bytes (60)`, which mdsmith passes. - `duplicate-heading` does 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 `# Build` inside a fence, gomarklint prints `duplicate heading: "build"`; mdsmith reports nothing. - `link-fragments` resolves `#anchor` links within one file only; it has no cross-file `other.md#section` resolution. mdsmith's [MDS027][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 broken `other.md#nope`, gomarklint stays silent while MDS027 reports `broken 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][gml-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](#benchmarks) 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][bench] 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`][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][vale-checks] (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][vale-action] and [VS Code extension][vale-vscode] ### [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 `--fix` for rules implementing the fixer API - [MCP server][textlint-mcp] 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][hugo-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 | `` / `` | | File inclusion | `{{< readfile >}}` shortcode | `` 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][hugo-migration] 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][obsidian-fm] (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][obsidian-linter]) add YAML front matter fixes, heading normalization, and whitespace rules - Files are plain `.md` on 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][mds027] | | Callouts | Native (`> [!note]`) | Validated by [MDS067][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][conventions]) to enable both checks with one config line. mdsmith also ships an [Obsidian plugin](../guides/editors/obsidian.md) 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][mdcov] 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][mds020] plus a CUE schema. obsidian-linter rewrites YAML *content*. - `yaml-key-sort` — sorts the keys - `yaml-title` / `yaml-title-alias` — derives them from the H1 - `yaml-timestamp` — normalises date fields - `format-yaml-array` / `sort-yaml-array-values` / `dedupe-yaml-array-values` — normalises arrays - `move-tags-to-yaml` — promotes inline tags into the front matter - `format-tags-in-yaml` — normalises tag syntax - `escape-yaml-special-characters` / `force-yaml-escape` — escapes string values - `insert-yaml-attributes` — fills missing keys - `remove-yaml-keys` — strips unwanted keys - `add-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 before - `move-footnotes-to-the-bottom` — collects reference definitions - `re-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-paste` - `prevent-double-checklist-indicator-on-paste` - `prevent-double-list-item-indicator-on-paste` - `proper-ellipsis-on-paste` - `remove-hyphens-on-paste` - `remove-leading-or-trailing-whitespace-on-paste` - `remove-leftover-footnotes-from-quote-on-paste` - `remove-multiple-blank-lines-on-paste` Two **Obsidian-specific** rules apply only to vault notes: - `capitalize-headings` — style preference - `file-name-heading` — the first H1 must match the filename stem The rest are **prose helpers** none of the other linters touch: - `auto-correct-common-misspellings` - `proper-ellipsis` and `quote-style` — straight to typographic forms - `remove-hyphenated-line-breaks` — joins words split by line-ending hyphens - `remove-multiple-spaces`, `remove-space-around-characters`, `remove-space-before-or-after-characters` - `two-spaces-between-lines-with-content` — forces the Markdown hard-break form - `space-between-chinese-japanese-or-korean-and-english-or-numbers` - `convert-bullet-list-markers`, `remove-consecutive-list-markers`, `remove-empty-list-markers`, `remove-empty-lines-between-list-markers-and-checklists` - `remove-link-spacing` — close to MD039 but applied as autofix - `compact-yaml` — collapses blank lines inside the front matter - `convert-spaces-to-tabs` — inverse of MD010 - `empty-line-around-horizontal-rules` - `empty-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: ```markdown --- 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); `mdsmith list backlinks` queries the link graph in reverse | 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 | | Rename | renames a heading (rewriting every workspace anchor link) or a link-ref label within one file; no file moves — after a `git mv`, MDS027 flags every broken incoming link | 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** has narrowed on the link layer. mdsmith now renames a heading with every workspace anchor link rewritten, or a link-reference label within its own file (`mdsmith rename`). It lists a file's incoming Markdown links with `mdsmith list backlinks`, and its wider dependency edges — includes, catalogs, builds, and links — with `mdsmith deps`, outgoing by default or incoming with `--incoming`. mdbase alone still ships the file-move rename, typed CRUD, watch mode, and the optional SQLite index. mdsmith alone still ships the prose, structure, and generated-content rules. Either surface is a snapshot, not a charter — see the deep-dive for evolutionary candidates either way. See the [deep-dive comparison][mdbase-deep-dive]. 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.md` file. 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][gptlint-cost]). AI review services: - [GitHub Copilot code review][copilot-review] (reviews Markdown in PRs) - [CodeRabbit][] (combines 40+ linters with LLM review) - [Claude Code Action][claude-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][coedit-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][mdcov]: 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][mdcov] 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][mds023] (ARI) | [metric][vale-metric] ext | yes | | Sentence limits | [MDS024][mds024] | [occurrence][vale-occurrence] ext | yes | | Word choice | no | [substitution][vale-substitution] ext | yes | | Passive voice | no | [existence][vale-existence] ext | yes | | Jargon detection | no | [existence][vale-existence] ext | yes | | Conciseness | [MDS029][mds029] (experimental) | no | yes | | Tone enforcement | no | custom styles | yes | | Token budget | [MDS028][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][mds025] | yes | no | MD055/56/58 | no | yes | via plugin | | Prose wrapping | opt-in (reflow) | [`proseWrap`][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`][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][mds027] | no | [remark-validate-links][rl-vl] | no | no | no | | Include sections | [MDS021][mds021] | no | no | no | no | no | | Catalog generation | [MDS019][mds019] | no | no | no | no | no | | Required structure | [MDS020][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][rl-fm] | 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][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 `...` block ([MDS038][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 registry. 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][bench] 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][bench] 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 world. Widest community adoption, most editor integrations, battle-tested rule set. **remark-lint** suits projects deep in the unified/remark stack (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][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][mds019])**: reads front matter fields from matched files to build summary tables. Fields become template variables (`{title}`, `{status}`). - **required-structure ([MDS020][mds020])**: validates document headings and front matter against a template. Supports CUE schemas for field types and constraints. - **include ([MDS021][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][mds020] rule. [MDS020][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][rl-fm] 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][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][myst] | Python | `{include} directive` | ~400 | | [Gitdown][] | Node.js | `<<< file.md` | ~460 | | [mdpre][] | Python | preprocessor directives | ~20 | Key differences from mdsmith's include rule ([MDS021][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 space. mdsmith ran a [10-finding adversarial review][mdsmith-sec]. 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][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][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_modules` to audit. Supply-chain risk is the Go module graph in `go.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 `` directive and its recipe-safety rule ([MDS040][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. [mds001]: ../../internal/rules/MDS001-line-length/README.md [mds019]: ../../internal/rules/MDS019-catalog/README.md [mds040]: ../../internal/rules/MDS040-recipe-safety/README.md [mds020]: ../../internal/rules/MDS020-required-structure/README.md [mds021]: ../../internal/rules/MDS021-include/README.md [mds023]: ../../internal/rules/MDS023-paragraph-readability/README.md [mds024]: ../../internal/rules/MDS024-paragraph-structure/README.md [mds025]: ../../internal/rules/MDS025-table-format/README.md [mds027]: ../../internal/rules/MDS027-cross-file-reference-integrity/README.md [mds067]: ../../internal/rules/MDS067-callout-type/README.md [mds070]: ../../internal/rules/MDS070-same-file-anchor/README.md [mds028]: ../../internal/rules/MDS028-token-budget/README.md [mds029]: ../../internal/rules/MDS029-conciseness-scoring/README.md [mds035]: ../../internal/rules/MDS035-toc-directive/README.md [mds038]: ../../internal/rules/MDS038-toc/README.md [markdownlint]: https://github.com/DavidAnson/markdownlint [markdownlint-cli2]: https://github.com/DavidAnson/markdownlint-cli2 [markdownlint-github]: https://github.com/github/markdownlint-github [mdl-vscode]: https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint [mdl-action]: https://github.com/DavidAnson/markdownlint-cli2-action [md001]: https://github.com/DavidAnson/markdownlint/blob/main/doc/md001.md [md059]: https://github.com/DavidAnson/markdownlint/blob/main/doc/md059.md [remark-lint]: https://github.com/remarkjs/remark-lint [remark]: https://github.com/remarkjs/remark [unified]: https://github.com/unifiedjs/unified [rl-vl]: https://github.com/remarkjs/remark-validate-links [rl-fm]: https://github.com/remarkjs/remark-frontmatter [markdown-include]: https://github.com/cmacmackin/markdown-include [MarkdownPP]: https://github.com/amyreese/markdown-pp [Markedpp]: https://github.com/commenthol/markedpp [myst]: https://mystmd.org/guide/embed [Gitdown]: https://github.com/gajus/gitdown [mdpre]: https://github.com/MartinPacker/mdpre [Prettier]: https://prettier.io/ [prosewrap]: https://prettier.io/docs/options#prose-wrap [Vale]: https://github.com/errata-ai/vale [vale-checks]: https://vale.sh/docs/checks/existence [vale-existence]: https://vale.sh/docs/checks/existence [vale-substitution]: https://vale.sh/docs/checks/substitution [vale-occurrence]: https://vale.sh/docs/checks/repetition [vale-metric]: https://vale.sh/docs/checks/metric [vale-action]: https://github.com/errata-ai/vale-action [vale-vscode]: https://marketplace.visualstudio.com/items?itemName=errata-ai.vale-server [textlint]: https://github.com/textlint/textlint [textlint-mcp]: https://textlint.org/docs/mcp/ [VectorLint]: https://github.com/TRocket-Labs/vectorlint [GPTLint]: https://github.com/gptlint/gptlint [GritQL]: https://docs.grit.io/language/overview [gptlint-cost]: https://gptlint.dev/project/cost [copilot-review]: https://docs.github.com/copilot/using-github-copilot/code-review/using-copilot-code-review [CodeRabbit]: https://www.coderabbit.ai/ [claude-action]: https://github.com/anthropics/claude-code-action [CoEdIT]: https://github.com/vipulraheja/coedit [coedit-paper]: https://arxiv.org/abs/2305.09857 [Hugo]: https://gohugo.io/ [hugo-shortcodes]: https://gohugo.io/content-management/shortcodes/ [hugo-migration]: ../guides/directives/hugo-migration.md [Obsidian]: https://obsidian.md/ [obsidian-fm]: https://help.obsidian.md/Editing+and+formatting/Obsidian+Flavored+Markdown [obsidian-linter]: https://github.com/platers/obsidian-linter [mdbase]: https://mdbase.dev/ [mdbase-deep-dive]: ../research/mdbase-vs-mdsmith/README.md [rumdl]: https://github.com/rvben/rumdl [mado]: https://github.com/akiomik/mado [panache]: https://panache.bz/ [gomarklint]: https://github.com/shinagawa-web/gomarklint [mdsmith-sec]: ../security/2026-04-05-adversarial-markdown/report.md [conventions]: ../reference/conventions.md [bench]: ../research/benchmarks/README.md [mdcov]: ../research/markdownlint-coverage/README.md [gml-evidence]: ../research/gomarklint-equivalence/README.md