Markdown Lint: Fix Formatting Errors with markdownlint

September 11, 2026 · 10 min read

Markdown Lint: Catch Formatting Errors with markdownlint

A markdown lint pass checks .md files against a rule set and flags the lines that break it. Typical catches: missing blank lines, bare URLs, unfenced code, inconsistent list markers. This guide covers the rules you'll actually hit, a starter config, the CLI and VS Code setup, and how to run it in CI.

What Does a Markdown Linter Check?

The tool parses each file the way a renderer would, then compares the result against numbered rules. The de facto standard is markdownlint, a Node.js library by David Anson. Its rule list currently runs from MD001 to MD060, and every rule has a readable alias (MD013 is line-length, MD040 is fenced-code-language).

Most of those rules are about consistency rather than correctness. A file with tabs and spaces mixed in its lists still renders, but the next person to edit it will guess wrong. A few rules catch real rendering bugs, and those are the ones worth turning on first.

Two things a linter doesn't do: it doesn't check spelling, and it doesn't verify that links resolve. For dead links you want a separate markdown validator such as markdown-link-check or lychee, which fetch every URL and report the failures. For style, a linter enforces the conventions you pick; it doesn't pick them for you.

If you're wondering whether you need this at all: a single README rarely does. A docs folder with more than one contributor almost always does, because the third person to edit a file will use a different list marker than the first two. The linter settles the argument before it starts.

The 10 markdownlint Rules You'll Hit First

Run the linter on any real project and the same handful of rules produce most of the output. Here they are with the broken version and the fix.

MD022 blanks-around-headings. A heading needs a blank line above and below. Without one, some parsers glue the heading to the previous paragraph.

Some text.
## Heading
More text.

Fix: add an empty line before and after ## Heading.

MD032 blanks-around-lists. Same rule for lists. This one bites hard because not every renderer starts a list directly after a paragraph: CommonMark lets a bullet interrupt a paragraph, but an ordered list that doesn't begin at 1 can't, and older Markdown.pl-style parsers glue the items onto the paragraph above.

Steps to install:
- Download the file
- Run the installer

Fix: blank line after Steps to install:. Our Markdown lists guide covers the other list rules, MD004 (consistent markers) and MD007 (indentation).

MD040 fenced-code-language. A fence with no language gets no highlighting and, on some platforms, gets treated as plain text with odd wrapping. Write ```bash instead of a bare ```.

MD034 no-bare-urls. See https://example.com for details is not a link in strict CommonMark. Wrap it in angle brackets, <https://example.com>, or use [text](url).

MD041 first-line-h1. The first line of the file should be a top-level heading. Files that start with a paragraph or an H2 fail. The Markdown headings guide covers this and MD001, which stops you jumping from H1 to H3.

MD013 line-length. Lines over 80 characters fail by default. Most teams either raise the limit or disable it, and the VS Code extension disables it out of the box for that reason.

MD033 no-inline-html. Any raw HTML tag is flagged. Useful on GitHub-only projects, annoying on sites that rely on <details> or <br />. Allow specific tags with the allowed_elements option.

MD024 no-duplicate-heading. Two headings with identical text break auto-generated anchors. Changelogs with repeated "Fixed" sections trip this constantly; set siblings_only: true to allow duplicates under different parents.

MD009 no-trailing-spaces. Trailing whitespace is invisible and usually accidental. The exception is exactly two spaces, which is the Markdown line break, and the rule allows that by default (br_spaces: 2).

MD047 single-trailing-newline. The file should end with one newline, not zero and not three. Every --fix run handles this one for you.

Those ten account for most of what you'll see. The remaining rules cover things like emphasis style (MD049 and MD050), table formatting (MD055, MD056, MD058, and MD060), and link fragments (MD051). Read the full Rules.md in the markdownlint repo when a code you don't recognise shows up; each entry has a rationale and a fixed example.

How Do You Lint Markdown from the Command Line?

There are two CLIs, and the naming is confusing. markdownlint-cli is the original wrapper. markdownlint-cli2 is the newer one from the library's own author, and it's what the VS Code extension and the GitHub Action use. We prefer cli2 for new projects because its config and the editor's config are the same file.

Install and run it:

npm install markdownlint-cli2 --global
markdownlint-cli2 "**/*.md" "#node_modules"

The # prefix excludes a glob. Add --fix to rewrite the roughly 30 fixable rules in place:

markdownlint-cli2 --fix "docs/**/*.md"

If you're on the older markdownlint-cli, the equivalent flags are -f or --fix, -c or --config <file>, and -i or --ignore <glob>. It also honours a .markdownlintignore file with gitignore syntax.

Not on Node? rumdl is a Rust reimplementation that reads existing .markdownlint.json files. Install with pip install rumdl or cargo install rumdl, then run rumdl check .. It's a reasonable drop-in for the common rules, though rule coverage isn't identical, so check its docs before switching a large project.

A Starter Config You Can Copy

markdownlint-cli2 reads .markdownlint-cli2.jsonc first, then falls back to .markdownlint.jsonc, .markdownlint.json, .markdownlint.yaml, and .markdownlint.yml. The plain .markdownlint.jsonc form works with both CLIs and the editor extension, so that's the one to create.

{
  // Turn every rule on, then override below
  "default": true,

  // 120 is kinder than 80; skip tables and code, which are often wide
  "MD013": { "line_length": 120, "tables": false, "code_blocks": false },

  // Allow the HTML tags that render on GitHub and most doc sites
  "MD033": { "allowed_elements": ["br", "details", "summary", "kbd", "sup", "sub"] },

  // Repeated headings are fine under different parents (changelogs)
  "MD024": { "siblings_only": true },

  // Docs sites often set the H1 from front matter
  "MD041": false
}

Aliases work as keys too, so "line-length": false means the same as "MD013": false.

To silence a rule for one spot instead of the whole project, use an HTML comment in the file:

<!-- markdownlint-disable MD033 -->
<details><summary>Raw HTML block</summary>content</details>
<!-- markdownlint-enable MD033 -->

<!-- markdownlint-disable-next-line MD013 -->
A single very long line that you have a good reason to keep intact ...

The other forms are markdownlint-disable-line, markdownlint-disable-next-line, and markdownlint-disable-file. One warning: HTML comments are fine in plain Markdown but crash MDX, so on an MDX site keep the exceptions in the config file instead.

Try Lint Markdown Fixes in the Editor

The snippet below breaks MD022, MD032, MD034, and MD009 at once. Notice that the preview still looks fine: this editor, like GitHub, is forgiving and renders the list and autolinks the URL anyway. That is exactly why a linter is worth running. A stricter renderer, or a teammate's editor, would show the list glued to the paragraph and the URL as plain text. Add the blank lines and wrap the URL in angle brackets and the file passes on every platform.

Release notes

Version 2.1

Bug fixes shipped this week:

Version 2.0

Trailing spaces on this line are invisible.

43 words270 characters9 lines
Markdown

The formatter tool normalises spacing, list markers, and trailing whitespace before you lint, which clears most MD009, MD004, and MD012 noise in one pass.

Which Setup Should You Use: VS Code, CLI, pre-commit, or CI?

Pick the layer that matches when you want feedback.

LayerToolBest for
While typingVS Code markdownlint extensionSolo writers, instant feedback
Before commitpre-commit hook markdownlint-cli2Teams that already use pre-commit
On push or PRDavidAnson/markdownlint-cli2-actionBlocking bad Markdown from main
Ad hocmarkdownlint-cli2 in a terminalCleaning up an old docs folder

VS Code. Install the extension by David Anson. It reads the same .markdownlint.jsonc and marks violations with green squiggles. Two settings matter: "markdownlint.run": "onSave" if onType feels noisy, and "editor.codeActionsOnSave": { "source.fixAll.markdownlint": "explicit" } to auto-fix on save. The VS Code Markdown guide covers the rest of the editing setup.

pre-commit. Add the hook from the markdownlint-cli2 repo with id: markdownlint-cli2. It runs only on staged .md files, so it stays fast. Pair it with --fix in the hook args and most commits need no manual cleanup at all.

GitHub Actions. The official action takes globs, config, and fix inputs:

- uses: DavidAnson/markdownlint-cli2-action@v24
  with:
    globs: |
      README.md
      docs/**/*.md

One caveat to plan around: the three tools resolve ignore files slightly differently. The CLI reads .markdownlintignore, cli2 uses ignores in its config, and the extension has its own markdownlint.lintWorkspaceGlobs setting for workspace-wide runs. Put your excludes in .markdownlint-cli2.jsonc under "ignores" so cli2 and the Action agree, and the extension, which reads the same file, follows for the most part.

Common Markdown Lint Mistakes

Treating every rule as an error on day one. Running default: true on a five-year-old docs folder produces thousands of lines. Start with --fix, commit that, then fix the rest by rule, most common first.

Disabling MD013 instead of configuring it. Turning line length off entirely lets 400-character paragraphs through, which makes diffs unreadable. Raise it to 120 and exclude tables and code.

Linting generated files. API references produced by a doc generator will never pass and shouldn't need to. Add their folder to ignores rather than sprinkling disable comments through the output.

Markdown Linter FAQ

A markdown lint setup pays for itself the first time it catches a list that swallowed the paragraph above it. Start with the config above, run --fix once, and wire the action into CI so the rules stay enforced. To test a fix before committing, paste the file into the editor and check that the preview matches what you meant.