---
description: "How rumdl compares with markdownlint on rule coverage, config discovery, and the places where it deliberately behaves differently."
icon: lucide/arrow-left-right
hide:
- toc
---
# rumdl vs markdownlint
rumdl is a practical markdownlint alternative for teams that want faster checks,
a native binary, built-in formatting, or support for modern Markdown flavors.
It implements all 53 markdownlint rules and automatically discovers common
markdownlint configuration files. Start with a read-only check; custom
JavaScript rules and several intentional behavior differences still require
review before switching CI.
> **Last verified: August 2026.** Rule counts and behavior are tested in this
> repository. Performance values come from the dated, documented
> [Markdown linter benchmark](benchmarks.md).
<nav class="rm-page-jumps" aria-label="On this page">
<strong>On this page</strong>
<a href="#decision-summary">Decision</a>
<a href="#rule-coverage">Rule coverage</a>
<a href="#intentional-design-differences">Behavioral differences</a>
<a href="#migration-checklist">Migration</a>
<a href="#feature-comparison-table">Feature matrix</a>
</nav>
## Try rumdl without changing the repository
From the repository root, run:
```bash
uvx rumdl check .
```
`uvx` runs rumdl in an isolated tool environment and may reuse its local cache.
rumdl automatically checks for supported markdownlint configuration files and
does not modify source files. Compare the diagnostics with your existing
command before changing configuration, editor, or CI settings.
| Existing markdownlint configuration with a faster native CLI | Try rumdl read-only |
| Built-in linting, formatting, LSP, and Markdown flavors | Try rumdl read-only |
| Custom lint rules written in JavaScript | Stay with markdownlint or replace the custom rules first |
| Exact bug-for-bug markdownlint behavior | Review the differences on this page before deciding |
Continue to the [migration checklist](#migration-checklist), or review the
[known behavioral differences](#known-behavioral-differences) first.
## Decision summary
rumdl offers **high markdownlint compatibility with intentional differences** while also adding performance improvements and newer features. All 53 markdownlint rules are implemented, but rumdl prefers predictable CommonMark-oriented behavior over bug-for-bug compatibility in a few documented areas.
**Key Differences:**
- **Performance**: the [dated, published Rust Book benchmark](benchmarks.md)
compares cold-start command latency with application caches disabled
- **Rule Coverage**: All 53 markdownlint rules are implemented, with a small number of intentional behavioral differences documented below
- **Unique Features**: 29 additional rules (MD057, MD061-MD088), built-in LSP server, VS Code extension, and built-in Markdown flavors
- **Configuration**: Automatic markdownlint config discovery and conversion
## Rule Coverage
### Implemented Rules
rumdl implements **<!-- RULE_COUNT -->82<!-- /RULE_COUNT --> rules total**: all 53 markdownlint rules plus <!-- RULE_COUNT_ADDITIONAL -->29<!-- /RULE_COUNT_ADDITIONAL --> unique rules.
**Markdownlint-compatible rules (53):** All markdownlint rule IDs are
implemented with high compatibility. Intentional behavioral differences are
documented below. See the [Rules Reference](rules.md) for the complete list.
**Note:** Rule numbers MD001-MD060 have gaps (MD002, MD006, MD008, MD015-MD017 were never implemented in markdownlint). rumdl maintains these gaps for compatibility.
### Rules Unique to rumdl
rumdl implements <!-- RULE_COUNT_ADDITIONAL -->29<!-- /RULE_COUNT_ADDITIONAL --> additional rules not found in markdownlint:
| MD057 | Relative links | Validates that relative file links point to existing files |
| MD061 | Forbidden terms | Flags usage of configurable forbidden terms |
| MD062 | Link destination whitespace | No whitespace in link destinations |
| MD063 | Heading capitalization | Enforces consistent heading capitalization style |
| MD064 | No multiple consecutive spaces | Flags multiple consecutive spaces in content |
| MD065 | Blanks around horizontal rules | Horizontal rules should have surrounding blank lines |
| MD066 | Footnote validation | Validates footnote references have definitions |
| MD067 | Footnote definition order | Footnotes should appear in order of reference |
| MD068 | Empty footnote definitions | Footnote definitions should not be empty |
| MD069 | No duplicate list markers | Flags duplicate markers like `- - text` from copy-paste |
| MD070 | Nested code fence | Detects nested fence collisions (opt-in) |
| MD071 | Blank line after frontmatter | Frontmatter should be followed by a blank line |
| MD072 | Frontmatter key sort | Frontmatter keys should be sorted (opt-in) |
| MD073 | TOC validation | Table of Contents should match headings (opt-in) |
| MD074 | MkDocs nav validation | Validates MkDocs nav entries against the docs tree |
| MD075 | Orphaned table rows | Detects headerless pipe tables and orphaned table rows |
| MD076 | List item spacing | Enforces consistent blank lines between list items |
| MD077 | List continuation indent | Enforces indentation for list continuation content |
| MD078 | Missing chunk labels | Executable Quarto chunks should have a label |
| MD079 | Chunk label spaces | Quarto chunk labels must not contain whitespace |
| MD080 | Heading anchor collision | Heading anchors (slugs) must be unique |
| MD081 | No excessive emphasis | Flags excessive bold/italic emphasis; off until configured |
| MD082 | No empty sections | Headings must have content before the next heading (opt-in) |
| MD083 | Detect mojibake | Detects mojibake caused by encoding issues (opt-in) |
| MD084 | Invisible characters | Detects hidden Unicode characters that should be intentional |
| MD085 | Paragraph continuation indent | Paragraph continuation lines should not be indented (opt-in) |
| MD086 | No unclosed comments | Flags a comment opener that nothing closes |
| MD087 | Unused disable comment | Flags a disable comment that suppressed nothing (opt-in) |
| MD088 | Quotes and dashes | Normalizes configured Unicode punctuation to ASCII (opt-in) |
**Opt-in rules:** MD060, MD063, MD070, MD072, MD073, MD074, MD080, MD082,
MD083, MD084, MD085, MD087, and MD088 are disabled by default. Enable them
explicitly in your configuration.
## Intentional Design Differences
### 1. CommonMark Specification Compliance
**rumdl prioritizes CommonMark specification compliance** over bug-for-bug compatibility with markdownlint's parsing.
**Example - List Continuation vs Code Blocks:**
```markdown
1. List item
This is a continuation paragraph (4 spaces = continuation)
This is a code block (8 spaces = continuation indent + 4)
```
- **markdownlint**: May incorrectly treat 4-space indented paragraphs as code blocks
- **rumdl**: Follows CommonMark: 4 spaces = list continuation, 8 spaces = code block within list
- **Rationale**: Reduces false positives and aligns with the official Markdown spec
**References:**
- [CommonMark List Specification](https://spec.commonmark.org/0.31.2/#lists)
- [rumdl Issue #128](https://github.com/rvben/rumdl/issues/128) - False positive fix
### 2. Performance Architecture
**rumdl uses Rust and intelligent caching** for significant performance gains:
- **Cold start**: See the [dated, published Rust Book benchmark](benchmarks.md)
for measured results, methodology, and limitations
- **Incremental**: Only re-lints changed files (Ruff-style caching)
- **Parallel processing**: Multi-threaded file processing and rule execution
- **Zero dependencies**: Single binary, no Node.js runtime required
**Benchmark:** See the [dated, published benchmark](benchmarks.md),
including the commands, scope, interpretation, and limitations for the Rust
Book repository workload.
### 3. Auto-fix Mode Differences
Both tools support auto-fixing, but with different philosophies:
**markdownlint:**
- Fixes issues in-place
- Requires `--fix` flag
**rumdl:**
- Two modes: `rumdl fmt` (formatter-style, exits 0) and `rumdl check --fix` (linter-style, exits 0 if all violations fixed, 1 if violations remain)
- `--diff` mode to preview changes
- Parallel file fixing for multi-file projects
**Why two modes?**
- `fmt`: Designed for editor integration (doesn't fail on unfixable issues)
- `check --fix`: Designed for CI/CD (fails if violations remain after fixing)
### 4. Configuration Philosophy
**Automatic Discovery:**
rumdl automatically discovers and loads both markdownlint and markdownlint-cli2 config files:
```bash
# rumdl automatically finds and uses these (in precedence order):
.markdownlint-cli2.jsonc
.markdownlint-cli2.yaml
.markdownlint-cli2.yml
.markdownlint.json
.markdownlint.yaml
markdownlint.json
```
For markdownlint-cli2 files, rumdl extracts rule configuration from the `config:` key and ignores cli2-specific keys like `globs` and `ignores`.
**Conversion Tool:**
```bash
# Convert markdownlint config to rumdl format:
rumdl import .markdownlint.json --output .rumdl.toml
```
**Multiple Formats:**
- Native: `.rumdl.toml` (TOML, with JSON schema support)
- Python projects: `pyproject.toml` with `[tool.rumdl]` section
- Markdownlint: Automatic compatibility mode
### 5. Editor Integration
**rumdl includes a built-in Language Server Protocol (LSP) implementation:**
```bash
# Start LSP server
rumdl server
# Install VS Code extension
rumdl vscode
```
**Features:**
- Real-time linting as you type
- Quick fixes for supported rules
- Hover documentation for rules
- Zero configuration required
### 6. Markdown Flavors
**rumdl provides built-in Markdown flavors** that adapt rule behavior for
different documentation systems. The complete current list is maintained in
the [flavors guide](flavors.md).
| `standard` | Default Markdown | CommonMark + GFM extensions |
| `gfm` | GitHub Flavored Markdown | Security-sensitive HTML, autolinks |
| `mkdocs` | MkDocs / Material for MkDocs | Admonitions, tabs, mkdocstrings |
| `mdx` | MDX (JSX in Markdown) | JSX components, ESM imports |
| `obsidian` | Obsidian knowledge base | Callouts, Dataview, Templater, wikilinks |
| `quarto` | Quarto / RMarkdown | Citations, shortcodes, executable code |
| `pandoc` | Pandoc Markdown | Fenced divs, attributes, citations |
| `kramdown` | Jekyll / kramdown | IALs, ALDs, extension blocks |
| `azure_devops` | Azure DevOps wikis | Colon code fences |
| `myst` | MyST / Jupyter Book / Sphinx | Directives, roles, `%` comments |
| `hugo` | Hugo / Goldmark | Block attribute lists |
| `mdg` | Markdown with Gherkin | Gherkin-safe headings, tags, and tables |
**Configuration:**
```toml
[global]
flavor = "mkdocs"
[per-file-flavor]
"docs/**/*.md" = "mkdocs"
"**/*.mdx" = "mdx"
```
markdownlint does not have built-in flavor support; users must configure individual rules manually.
### 7. Rule-Specific Default Differences
A few rules ship safer defaults than markdownlint. These are opt-out, not removed - set the documented option to recover markdownlint-exact behavior.
**MD010 (Hard tabs) - `code_blocks`:**
- **markdownlint**: defaults `code_blocks: true`, flagging and rewriting tabs inside fenced *and* indented code blocks.
- **rumdl**: defaults `code-blocks = false`, skipping tabs inside both fenced and indented code blocks.
- **Rationale**: tabs are syntactically required in Makefiles and conventional in `gofmt`-formatted Go. markdownlint's default silently corrupts such snippets on auto-fix. Skipping code blocks by default is strictly safer.
- **markdownlint parity**: set `code-blocks = true` to flag tabs everywhere, including code blocks.
```toml
[MD010]
code-blocks = true # markdownlint-exact behavior
```
**Reference:** [rumdl Issue #630](https://github.com/rvben/rumdl/issues/630) - inconsistent tab handling between fenced and indented code blocks.
## Configuration Compatibility
### Markdownlint Config Auto-Detection
rumdl automatically discovers and loads markdownlint and markdownlint-cli2 configurations:
```yaml
# .markdownlint.yaml (automatically loaded)
MD013: false
MD033:
allowed_elements: ['br', 'img']
```
```yaml
# .markdownlint-cli2.yaml (also automatically loaded)
config:
MD013: false
MD033:
allowed_elements: ['br', 'img']
```
### Equivalent rumdl Configuration
```toml
# .rumdl.toml
[global]
disable = ["MD013"]
[MD033]
allowed_elements = ["br", "img"]
```
### Configuration Mapping
Most markdownlint options map directly to rumdl:
| `default: true` | `[global]` section |
| Rule by number (`MD013`) | Same (`[MD013]`) |
| Rule by name (`line-length`) | Same (`[line-length]`) |
| Disabling: `"MD013": false` | `disable = ["MD013"]` |
### Per-File Ignores
Both support per-file rule configuration:
```toml
# rumdl
[per-file-ignores]
"README.md" = ["MD033"] # Allow HTML in README
"docs/api/**/*.md" = ["MD013"] # Relax line length in API docs
```
markdownlint uses glob patterns in separate config files or inline comments.
## Inline Configuration Compatibility
rumdl supports both `rumdl` and `markdownlint` inline comment styles:
```markdown
This line can be as long as needed
Alternative syntax also supported
```
Both syntaxes work identically in rumdl for seamless migration.
## CLI Differences
### Command Structure
**markdownlint:**
```bash
markdownlint README.md
markdownlint --fix **/*.md
markdownlint --config .markdownlint.json docs/
```
**rumdl:**
```bash
rumdl check README.md
rumdl check --fix . # or: rumdl fmt .
rumdl check --config .rumdl.toml docs/
```
### Output Formats
Both support:
- Text output (colored, human-readable)
- JSON output (for tool integration)
rumdl additionally supports:
- Source line display with caret underlines (`--output-format full`)
- GitHub Actions annotations (`--output-format github`)
- GitLab, Azure, SARIF, JUnit, and Pylint formats
- Statistics summary (`--statistics`)
- Profiling information (`--profile`)
### Exit Codes
**markdownlint-cli:**
- `0`: No violations
- `1`: Violations found
- `2`: Unable to write output
- `3`: Unable to load custom rules
- `4`: Unexpected error
**rumdl:**
- `0`: Success (or `rumdl fmt` completed successfully)
- `1`: Violations found (or remain after `--fix`)
- `2`: Tool error
## Migration checklist
### 1. Establish the current result
Run your existing markdownlint command on a clean worktree and retain its
diagnostics. This gives you a concrete result to compare instead of assuming the
tools behave identically.
### 2. Run a read-only rumdl check
```bash
uvx rumdl check .
```
rumdl discovers supported `.markdownlint.*` and `.markdownlint-cli2.*` files.
The check does not edit Markdown or replace the configuration.
### 3. Explain differences before suppressing them
Compare both outputs on representative files. Check the
[intentional differences](#known-behavioral-differences), especially rule
defaults that protect code samples or account for CommonMark parsing. Report an
unexpected compatibility difference instead of immediately disabling the rule.
### 4. Convert the configuration only if useful
Preview the conversion first:
```bash
rumdl import --dry-run .markdownlint.json
```
Then write the default `.rumdl.toml` when you are ready to maintain native
configuration:
```bash
rumdl import .markdownlint.json
```
Keeping the markdownlint file is also supported, so conversion is not required
for the first CI run.
### 5. Change CI after the local comparison passes
With the official GitHub Action:
```yaml
- uses: actions/checkout@v6
- uses: rvben/rumdl@v0
```
Or replace only the command in an existing job:
```yaml
# Before
- run: markdownlint '**/*.md'
# After
- run: rumdl check .
```
See the [CI/CD guide](usage/ci-cd.md) for annotations, formatting checks, and
version pinning.
### 6. Keep rollback simple
Do not remove the original markdownlint configuration during the evaluation.
If rumdl does not fit the repository, restore the previous CI command; the
read-only trial has not changed project files.
### Known Behavioral Differences
These are intentional deviations where rumdl produces different results than markdownlint. They are design decisions, not bugs.
**MD004 (unordered-list-style):** In `consistent` mode, rumdl uses prevalence-based detection (most common marker wins, ties prefer dash). markdownlint uses the first marker as the standard.
**MD005/MD007 (list-indent / ul-indent):** rumdl uses parent-based dynamic indentation, properly handling ordered lists with variable marker widths (e.g., `1.` vs `10.`). markdownlint may treat
children at different indentation levels as inconsistent.
**MD012 (no-multiple-blanks):** rumdl uses the `filtered_lines()` architecture to skip frontmatter, code blocks, and flavor-specific constructs. This may produce slightly different counts near block
boundaries.
**MD013 (line-length):** rumdl exempts entire lines that are completely unbreakable (URLs with no spaces, long code spans). It also supports `line_length = 0` to mean unlimited. markdownlint exempts
more selectively. rumdl supports markdownlint's `stern`, `heading_line_length`, and `code_block_line_length` options for context-specific limits and a stricter trailing-token policy. One known
divergence: markdownlint exempts heading lines that contain any link, autolink, or image (its `linkOnlyLineNumbers` covers headings because heading text is not classified as paragraph data); rumdl
applies its URL/inline-link suppression to heading lines but does not exempt heading lines that contain bare autolinks. Affected lines can be tagged with `<!-- rumdl-disable-line MD013 -->` if needed.
rumdl also adds a `math-blocks` option (default `true`) that exempts display-math (`$$ ... $$`) lines from the length check, and never reflows a multi-line math block, since LaTeX cannot be rewrapped
without changing the equation. markdownlint has no math-aware handling here.
**MD027 (no-multiple-space-blockquote):** rumdl's `list-items` option defaults to `false`; markdownlint's `list_items` defaults to `true`. List items inside blockquotes inherently need extra
indentation (`> - item`, continuation lines), so flagging them by default produces noise. Set `list-items = true` to opt into strict markdownlint behavior.
**MD029 (ordered-list-prefix):** rumdl uses CommonMark AST start values. A list starting at `11` expects items 11, 12, 13. rumdl only auto-fixes when `start_value == 1` to preserve explicit numbering
intent.
**MD051 (link-fragments):** rumdl's `ignore-case` option defaults to `true`; markdownlint's `ignore_case` defaults to `false`. Permissive matching better fits multi-platform docs (where some
processors lowercase fragments and others don't), and it preserves rumdl's long-standing behavior. Set `ignore-case = false` to opt into strict markdownlint parity. The companion `ignored-pattern`
option matches markdownlint exactly.
If you encounter other compatibility issues, please [file an issue](https://github.com/rvben/rumdl/issues).
## Feature Comparison Table
| **Core Functionality** | | |
| Rule count | 53 implemented | 82 (53 compatible + 29 new) |
| Auto-fix | ✅ | ✅ |
| Configuration file | ✅ JSON/YAML | ✅ TOML/JSON/JSONC/YAML/cli2 |
| Inline config | ✅ | ✅ (compatible) |
| Custom rules | ✅ (JavaScript) | ❌ |
| Markdown flavors | ❌ | ✅ Built in |
| **Performance** | | |
| Rust Book cold start | See [benchmark](benchmarks.md) | See [benchmark](benchmarks.md) |
| Incremental mode | ❌ | ✅ (caching) |
| Parallel processing | Partial | ✅ Full |
| **Developer Experience** | | |
| Built-in LSP | ❌ | ✅ |
| VS Code extension | ✅ (separate) | ✅ (built-in) |
| Watch mode | Via external tools | ✅ `--watch` |
| Stdin/stdout | ✅ | ✅ |
| Diff preview | ❌ | ✅ `--diff` |
| **Installation** | | |
| Node.js required | ✅ | ❌ |
| Python pip | ❌ | ✅ |
| Rust cargo | ❌ | ✅ |
| Single binary | ❌ | ✅ |
| Homebrew | ✅ | ✅ |
| **Output & Integration** | | |
| Text format | ✅ | ✅ |
| JSON format | ✅ | ✅ |
| GitHub Actions | ✅ | ✅ Enhanced |
| Statistics | ❌ | ✅ |
| Profiling | ❌ | ✅ |
## CommonMark Compliance
rumdl prioritizes **CommonMark specification compliance** to reduce false positives and align with modern Markdown standards:
| List continuation indent | Custom logic | CommonMark spec |
| Code blocks in lists | May misdetect | Spec-compliant (8 spaces) |
| Heading anchor generation | GitHub-flavored | Multiple styles (GitHub, GitLab, etc.) |
| Reference definitions | Basic | Full spec support |
### Reporting Compatibility Issues
If you find a compatibility issue with markdownlint:
1. Check [existing issues](https://github.com/rvben/rumdl/issues?q=is%3Aissue+label%3Acompatibility)
2. Verify with both tools: `markdownlint file.md` and `rumdl check file.md`
3. [File an issue](https://github.com/rvben/rumdl/issues/new) with:
- Markdown sample
- Expected behavior (markdownlint output)
- Actual behavior (rumdl output)
- Versions of both tools
## See Also
- [Tool Comparison Matrix](comparison.md) - Broad comparison of Markdown linters and formatters
- [Benchmark Methodology](benchmarks.md) - Reproduce and interpret the cold-start results
- [Comparison with mdformat](mdformat-comparison.md) - For users coming from the mdformat formatter
## References
- [markdownlint documentation](https://github.com/DavidAnson/markdownlint)
- [CommonMark specification](https://spec.commonmark.org/)
- [rumdl GitHub repository](https://github.com/rvben/rumdl)
- [rumdl rules documentation](rules.md)
- [rumdl flavors documentation](flavors.md)