Self-hosted service
DavidAnson/markdownlint-cli2 avatar
DavidAnson/markdownlint-cli2

markdownlint-cli2: the configuration-first command line linter for Markdown

A fast, flexible, configuration-based command-line interface for linting Markdown/CommonMark files with the markdownlint library

955 stars83 forksJavaScriptMIT

At a glance

What is it?
markdownlint-cli2 wraps the markdownlint library in a Node.js CLI that reads its rules from configuration files, understands glob patterns, and can rewrite files in place. It is a second front end for a library that already had one, and the interesting part is what the extra layer changes for CI and for editor workflows.
Who is it for?
Adopt markdownlint-cli2 if your Markdown is linted in CI or by an editor and you want the rules in versioned configuration files rather than in command lines. Do not adopt it if you only need to check one file by hand, since the overhead of a config layer buys nothing there.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A second command line for a library that already had one

The naming makes this project look like a fork. It is not. The documentation lays out the family explicitly. `markdownlint` is a library for linting Markdown and CommonMark files on Node.js using the markdown-it parser. `markdownlint-cli` is the traditional command-line interface for that library. `markdownlint-cli2` is a slightly unconventional command-line interface for the same library.

That distinction is the whole design. The project description calls it configuration-based and says it prioritizes speed and simplicity, and the two properties are linked. A traditional CLI tends to grow flags, because the command line is the only place to put options. A configuration-first CLI can keep a very small argument surface and let a file carry the rest.

The documentation states that markdownlint-cli2 supports all the features of markdownlint-cli, sometimes a little differently. That phrase is doing quiet work. It is a claim of feature parity, not of behavioural identity, and the two interfaces are not interchangeable without checking.

The intended user is someone who lints Markdown as part of a build, a pull request check, or an editor workflow. The intended non-user is someone with three Markdown files and a text editor, for whom installing a Node.js package is more ceremony than the problem deserves.

The dot glob that lints far more than you meant

The single most useful thing to know before running this is a trap. The documentation explains that the command `markdownlint-cli2 .` would lint every file in the current directory tree, which is probably not intended, so a dot-only glob is mapped to `markdownlint-cli2 *.{md,markdown}`, which lints all Markdown files in the current directory. If you really do want the whole tree, you write `markdownlint-cli2 **`.

That is a helpful default, and it is also a silent override of what you typed. A reviewer who sees a dot in a CI script should know it does not mean what it looks like.

The glob syntax comes from the globby library and the documentation spells out the forms: `*` matches any number of characters but not the path separator, `?` matches a single character but not the separator, `**` matches any number of characters including the separator, `{}` allows a comma-separated list of or expressions, a leading `!` or `#` negates the match, a leading `:` identifies a literal file path, and a single `-` stands for standard input.

The `!` and `#` alternatives exist for a reason that shows up immediately in the command line examples. The documented cross-platform form uses `#node_modules` rather than `!node_modules`, because some UNIX shells parse an exclamation mark inside double quotes. The same section notes that Windows shells handle single-quoted arguments badly, so double quotes are recommended, and that shells which expand globs at all cannot handle a negated pattern without quoting.

Four ways to install, and which one fits your pipeline

The documentation gives four install paths, and picking between them is really a question about where the lint step runs.

As a global CLI on a developer machine:

bash
npm install markdownlint-cli2 --global

As a development dependency of the current Node.js package, which is the version that belongs in a repository that runs in CI:

bash
npm install markdownlint-cli2 --save-dev

As a container image, for a pipeline that does not want a Node.js toolchain installed:

bash
docker pull davidanson/markdownlint-cli2

And as a Homebrew package on macOS and Linux:

bash
brew install markdownlint-cli2

A fifth path is the GitHub Action, which is a separate repository pulled in as a workflow step:

yaml
- name: markdownlint-cli2-action
  uses: DavidAnson/markdownlint-cli2-action@main

The standard invocation, once installed, looks like this:

bash
markdownlint-cli2 "**/*.md" "#node_modules"

The quoted glob matters. Without quotes the shell expands it before the tool sees it, and the exclusion of node_modules cannot be expressed, which is exactly the case the documentation warns about.

Fix mode, config discovery, and the JSON Pointer escape hatch

The `--fix` argument updates files to resolve fixable issues, and the documentation notes the reason it defaults the `fix` property to true: sharing the same configuration between normal and fix modes is common, though a configuration file can still override it.

bash
markdownlint-cli2 --fix "**/*.md" "#node_modules"

Configuration is discovered by filename. The documented names are `.markdownlint-cli2.jsonc`, `.markdownlint-cli2.yaml`, `.markdownlint-cli2.cjs` or `.markdownlint-cli2.mjs`, and the plain library names `.markdownlint.jsonc`, `.markdownlint.json`, `.markdownlint.yaml`, `.markdownlint.yml`, `.markdownlint.cjs` or `.markdownlint.mjs`. If a configuration file is inconvenient to place in the project root, `--config` accepts a path to any supported file or format, and the file name should be one of the supported names or carry a supported extension such as `.jsonc`, `.yaml`, `.mjs` or `.toml` so its kind can be inferred.

bash
markdownlint-cli2 --config "config/.markdownlint-cli2.jsonc" "**/*.md" "#node_modules"

The more interesting feature is `--configPointer`, which takes a JSON Pointer to a sub-object inside the file named by `--config`. The documentation's use case is nesting settings inside a file you already have, such as package.json, and the example puts a config block and a `noProgress` flag under a `markdownlint-cli2` key:

json
{
  "...": "...",
  "markdownlint-cli2": {
    "config": {
      "no-multiple-blanks": false
    },
    "noProgress": true
  }
}
bash
markdownlint-cli2 --config package.json --configPointer /markdownlint-cli2 "*.md"

The same trick works in a Python project file, with the pointer reaching into a TOML table:

toml
[project]
# ...

[tool.markdownlint-cli2]
noProgress = true

[tool.markdownlint-cli2.config]
no-multiple-blanks = false
bash
markdownlint-cli2 --config pyproject.toml --configPointer /tool/markdownlint-cli2 "*.md"

If you would rather not repeat globs on the command line at all, the documentation also records a `globs` property for the configuration file, which can be used instead of, or in addition to, the command-line globs.

TOML works almost everywhere, except for per-directory overrides

One limitation is called out explicitly and it is easy to trip over. The TOML format is supported by `--config`, by `--configPointer`, and by the `extends` configuration property, but not for per-directory overrides.

Per-directory overrides are the feature where a subdirectory gets its own rules, typically for generated documentation or vendored content that should not be held to the same standard as hand written pages. If your project uses that pattern and you want to keep the settings in TOML, this tool cannot do it. The other configuration formats listed, JSON, JSONC, YAML and the JavaScript and module variants, remain available for that case.

A second constraint is cross-platform behaviour, and the documentation devotes several lines to it. UNIX and Windows shells expand globs by different rules, so quoting arguments is recommended. The path separator is a forward slash on every platform, and a backslash is converted automatically. On any platform, passing `--` causes all remaining parameters to be treated literally, which is the escape hatch when a filename genuinely begins with a dash.

A third limitation is the input path. The `--format` argument reads standard input, applies fixes and writes to standard output, and the glob syntax offers `-` as standard input. That makes the tool usable in a pipe, but the documentation does not describe reading a document from a database or an API response as a first class use case, and the repository layout does not suggest one.

What the repository layout says about the plugin surface

The repository tree explains more about the design than the prose does. At the top level sit the entry points `markdownlint-cli2.mjs` and `markdownlint-cli2-bin.mjs`, the option merging logic in `merge-options.mjs`, and the shared constants in `constants.mjs`. The package exports more than the CLI: `./markdownlint`, `./markdownlint/helpers`, `./markdownlint/promise`, and `./parsers` with separate entry points for jsonc, toml and yaml parsing.

Then there is a row of formatter directories, `formatter-codequality`, `formatter-default`, `formatter-json`, `formatter-junit`, `formatter-pretty`, `formatter-sarif`, `formatter-summarize` and `formatter-template`, plus a `webworker` directory and a `parsers` directory. A JUnit formatter and a SARIF formatter exist, which tells you the intended consumers include CI systems that ingest machine readable reports, not only humans reading a terminal.

Two other files are worth noting for anyone planning to run this in a repository. There is a `.pre-commit-hooks.yaml`, so the project is prepared for a pre-commit hook workflow. And there is a `schema/` directory, which pairs with the configuration story: rules and configuration keys are described by a schema rather than left to documentation alone.

On upkeep, the licence field on the repository says MIT and the package version in package.json is 0.23.3. The repository is not archived and the last push was on 2026-10-19, and there are no GitHub releases, so version tracking happens in package.json and the changelog rather than through tagged releases.

Editorial conclusion

Adopt markdownlint-cli2 if your Markdown is linted in CI or by an editor and you want the rules in versioned configuration files rather than in command lines. Do not adopt it if you only need to check one file by hand, since the overhead of a config layer buys nothing there. Before switching, run the same glob against markdownlint-cli on your existing files and diff the two reports, because the documentation states that markdownlint-cli2 supports all the features of markdownlint-cli, sometimes a little differently.

Frequently asked questions

How do I install markdownlint-cli2 as a project dependency?

The documentation gives two npm forms: `npm install markdownlint-cli2 --global` for a global CLI and `npm install markdownlint-cli2 --save-dev` as a development dependency of the current Node.js package. A container image and a Homebrew package are also offered.

Why does markdownlint-cli2 not lint my node_modules directory?

You have to exclude it yourself with a negated glob. The documented cross-platform form is `markdownlint-cli2 "**/*.md" "#node_modules"`, and the documentation notes that shells which expand globs cannot handle a negated pattern unless it is quoted, which is why the hash form is used.

Can markdownlint-cli2 read its configuration from package.json?

Yes, through the `--config` and `--configPointer` arguments. The documentation shows a JSON Pointer such as `/markdownlint-cli2` used to reach a config object nested inside package.json, and the same mechanism works for a `[tool.markdownlint-cli2]` table in pyproject.toml.

Is markdownlint-cli2 a fork of markdownlint-cli?

No. The documentation describes markdownlint-cli as the traditional command-line interface and markdownlint-cli2 as a slightly unconventional one, both sitting on the same markdownlint library. It states that markdownlint-cli2 supports all the features of markdownlint-cli, sometimes a little differently.

Can markdownlint-cli2 fix problems in files automatically?

The `--fix` argument updates files to resolve fixable issues and defaults the `fix` property to true so the same configuration works in normal and fix modes, though a configuration file can still override it. The `--format` argument does the same over standard input and writes to standard output.

Official sources

  1. DavidAnson/markdownlint-cli2 on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/davidanson-markdownlint-cli2.svg)](https://hysenlabs.com/projects/davidanson-markdownlint-cli2)

Was this useful?