mdsmith
Esc
    v0.52.0 GitHub

    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 --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

    # 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

    # 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-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
    Aspectgomarklintmdsmith
    Distributionstatic Go binarystatic Go binary
    Rule set23, kebab-case IDs69, MDSxxx IDs
    Autofixnomdsmith fix, multi-pass
    LSP / editorno (VS Code planned)LSP for any editor
    Config.gomarklint.json.mdsmith.yml
    External URLsexternal-link (HTTP)not checked (offline by design)
    Cross-filenolinks, includes, catalogs, kinds
    Generatednocatalog, 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-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 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 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 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 --fix for 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:

    AspectHugomdsmith
    Generated outputSeparate public/ HTML treeIn-place inside the source .md
    Source readabilityTemplates obscure final bodySource always renders as-is
    ValidationNone (build succeeds or fails)Diagnostics + autofix
    TOC / list-of-filesShortcodes / list templates<?toc?> / <?catalog?>
    File inclusion{{< readfile >}} shortcode<?include?> directive
    Variable syntax{{ .Title }} (Go template){title} (front matter field)
    Merge conflictsRe-render at build timemerge-driver install resolves
    Agent friendlinessIndirect (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 .md on disk and are committed to Git like any other source; CI can run mdsmith over the vault
    AspectObsidianmdsmith
    PurposeNote-taking editorLinter / fixer
    LintingCommunity plugin onlyBuilt-in, CI-ready
    WikilinksNative ([[Page]])Validated by MDS027
    CalloutsNative (> [!note])Validated by MDS067
    Front matterYAML or Dataview inline (key::)YAML only (inline not recognized)
    Agent friendlinessEditor-centric, manual savesDirect 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
    Aspectobsidian-lintermdsmith
    DistributionObsidian pluginStatic Go binary
    ScopeActive note onlyWhole repo walk
    CI gatenomdsmith check
    EditorObsidian onlyLSP for any editor
    Configper-vault UI.mdsmith.yml in repo
    Autofix modelsave / paste / commandmdsmith fix, multi-pass
    Cross-filenolinks, includes, catalogs, kinds
    YAML rules14 (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 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:

    ---
    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:

    Layermdsmithmdbase
    YAML front matterreads it; can validate shape via CUE schemareads it; validates against _types/task.md
    Body content (prose, headings)lints line length, headings, prose, linksnot in scope
    Cross-file linkflags broken auth-migration-log.md (MDS027)flags broken link (L4) and rewrites it on rename (L5)
    status: in-progressavailable to mdsmith list queryfilterable in Bases queries; appears in backlink graphs
    due: 2026-06-01available to queryfilterable with date arithmetic (due <= today() + "7d")
    mdsmith fix runsreformats tables, regenerates TOC/catalogn/a
    mdbase rename runsn/amoves the file and rewrites every incoming link
    Body readability, structure, token budgetyes (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.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 ).

    AI review services:

    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.

    Aspectmdsmithrumdlmadopanache
    LanguageGoRustRustRust
    Rule IDsown MDSxxxmarkdownlint MDxxxmarkdownlint MDxxxown
    Rule count6971~41unenumerated
    Autofix / formatfix--fix, fmtnoformat
    LSP / editoryes (LSP)yes (LSP)noyes (LSP)
    Config formatYAMLTOMLTOMLTOML
    Reuse markdownlint cfgnoyesnono
    Cross-file integrityyesnonono
    Generated sectionsyesnonono
    Readability/token rulesyesnonono
    Front-matter schemayesnonono
    Quarto / R MarkdownnoQuarto flavornoyes (CST)

    # Prose and Readability

    CapabilitymdsmithValeLLM
    Readability gradeMDS023 (ARI)metric extyes
    Sentence limitsMDS024occurrence extyes
    Word choicenosubstitution extyes
    Passive voicenoexistence extyes
    Jargon detectionnoexistence extyes
    ConcisenessMDS029 (experimental)noyes
    Tone enforcementnocustom stylesyes
    Token budgetMDS028nono
    Deterministicyesyesno

    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

    CapabilitymdsmithPrettiermarkdownlintrumdlmadopanacheremark-lint
    Autofix CLIfix--write--fix--fix / fmtnoformatyes (AST rewrite)
    Table alignmentMDS025yesnoMD055/56/58noyesvia plugin
    Prose wrappingopt-in (reflow)proseWrapnonononono
    Embedded code fmtnoJS/TS/CSS/JSONnononodelegates to externalno
    Multi-pass fixyessingle passsingle passsingle passnosingle passsingle pass
    Generated sectionscatalog, include, tocnononononono

    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

    Capabilitymdsmithmarkdownlintremark-lintrumdlmadopanache
    Link integrityMDS027noremark-validate-linksnonono
    Include sectionsMDS021nonononono
    Catalog generationMDS019nonononono
    Required structureMDS020nonononono
    Front-matter querymdsmith list querynonononono
    Git merge driveryesnonononono
    Metrics rankingyesnonononono
    Gitignore awareyesyesnoyesyesyes
    Front matter supportyesvia pluginvia remark-frontmatteryesnoyes

    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

    Propertymdsmithmarkdownlintremark-lintPrettierValetextlintLLM
    LanguageGoNode.jsNode.jsNode.jsGoNode.jsAPI
    Runtime depsnoneNode 20+Node 16+Node 16+noneNode 20+network
    Installbinarynpmnpmnpmbinarynpmvaries
    Config formatYAMLJSONC/YAML/JSJSON/YAML/JSJSON/YAML/JSINI+YAMLJSON/YAML/JSprompt
    Output formatstext, JSONtext, JSONtextnonetext, JSONtext, JSONtext
    VS Codeyes (LSP)yesyesyesyesyesvaries
    GitHub Actionnoyesvia npmvia npmyesvia npmcustom
    Pre-commitlefthookhusky/lefthookhuskyhuskyhookshuskyimpractical
    Offlineyesyesyesyesyesyesno
    Deterministicyesyesyesyesyesyesno

    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.

    ToolLanguageInclude syntaxStars
    markdown-includePython{!filename!}~100
    MarkdownPPPython!INCLUDE "file.md"~350
    MarkedppNode.js!include(file.md)~50
    MyST MarkdownPython{include} directive~400
    GitdownNode.js<<< file.md~460
    mdprePythonpreprocessor 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:

    Hardeningmdsmithmarkdownlintremark-lintPrettierVale
    File-size cap on inputyesnononono
    YAML billion-laughs guardyes (alias rejection)n/a (no FM)parser-dependentparser-dependentn/a
    ANSI escape sanitizationyesnonon/ano
    Symlinks denied by defaultyesfollowsfollowsfollowsfollows
    Cross-file links sandboxed to repoyes (MDS027 )n/aplugin-dependentn/an/a
    Include size capyesn/an/an/an/a
    Schema/CUE path validationyes (MDS020 )n/an/an/an/a
    Atomic writes in fix modeyesn/an/ayesn/a
    Network access at runtimenonenonenonenonenone
    Dependency surfaceGo stdlib + goldmarkNode + npm treeNode + npm treeNode + npm treeGo 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 <?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.