Skip to main content

mdlinkcheck

CI PyPI License: MIT Python 3.9+

Find broken relative links and #anchors in Markdown files: offline, fast, with no dependencies, and using the same heading-anchor rules as GitHub.

$ mdlinkcheck src/engine
src/engine/prague.md:72:38: #BlobsBundleV1: no heading or anchor '#BlobsBundleV1' in this file [missing-anchor]
src/engine/refactor-ssz.md:23:23: #max-constants: no heading or anchor '#max-constants' in this file; did you mean '#max_-constants'? [missing-anchor]
src/engine/refactor-ssz.md:524:26: #get-forkpayloadspayloadid: no heading or anchor '#get-forkpayloadspayloadid' in this file; did you mean '#get-payloadspayloadid'? [missing-anchor]
src/engine/refactor.md:447:39: ./refactor-ssz.md#max-constants: no heading or anchor '#max-constants' in 'refactor-ssz.md'; did you mean '#max_-constants'? [missing-anchor]
mdlinkcheck: 13 files, 460 links, 4 broken

Why

External-URL checkers (lychee, markdown-link-check) are great at telling you a website is down. They are much less good at the links that break inside a repository:

  • a section gets renamed and every other-doc.md#old-title link silently stops scrolling anywhere;
  • a heading contains code, punctuation or underscores, so its real anchor isn't what you'd guess (## `MAX_*` constants is #max_-constants, ## Definition of `channel_id` is #definition-of-channel_id);
  • a file moves and ../dump.md becomes ./dump.md in one place but not another;
  • a link works on your Mac but not on GitHub, because the file name differs only in case.

GitHub doesn't warn about any of these. mdlinkcheck does, without touching the network, so it is fast enough for a pre-commit hook and deterministic enough for CI.

Install

pipx install mdlinkcheck
# or
pip install mdlinkcheck

Python 3.9 or newer; no third-party dependencies.

Usage

mdlinkcheck                 # every Markdown file in the repository
mdlinkcheck docs README.md  # specific files and directories

It exits with 1 if any link is broken, 0 otherwise, and 2 on usage errors.

Option Description
-e, --exclude GLOB Skip files matching GLOB (repeatable), e.g. -e 'vendor/*' -e CHANGELOG.md.
-i, --ignore REGEX Don't check link targets matching REGEX (repeatable), e.g. -i '^generated/'.
-s, --site-root DIR Pages directory of a docs site. /route links resolve against it, and links inside it may leave out .md. Repeatable, e.g. for a public/ assets folder. Replaces auto-detection.
--no-detect-sites Don't auto-detect docs sites (see below).
--no-absolute Don't check links that start with /.
--root DIR Directory that / links are relative to (default: the git top level).
--no-git Walk directories directly instead of using git ls-files (which skips ignored files).
-f, --format text, github (workflow annotations; the default inside GitHub Actions) or json.
-q, --quiet Don't print the summary line.

Docs sites

Docs generators serve pages by route, so their links often leave out .md or start with /. mdlinkcheck looks for docusaurus.config.*, vocs.config.*, .vitepress/ and mkdocs.yml and resolves links inside those sites the way the site will: against the pages folder and static assets folder, with or without the extension, and from the parent folder for index pages. The detected folders are printed in the summary; pass --site-root to set them yourself.

To skip a single link, put <!-- mdlinkcheck-ignore --> on the same line or on the line above it.

GitHub Actions

- uses: actions/checkout@v4
- uses: mikeua/mdlinkcheck@v0.2.0
  with:
    args: --exclude CHANGELOG.md .

Broken links show up as annotations on the pull request diff.

pre-commit

repos:
  - repo: https://github.com/mikeua/mdlinkcheck
    rev: v0.2.0
    hooks:
      - id: mdlinkcheck

What it checks

Rule Example
missing-file [guide](docs/guid.md) when there is no docs/guid.md (suggests close matches, such as a missing .md)
missing-anchor [x](#instal) or [x](api.md#old-name) when no heading, {#custom-id}, <a name>/id= or footnote produces that anchor (suggests the closest one)
case-mismatch [x](Guide.md) when the file is guide.md: fine on macOS and Windows, broken on GitHub and Linux

Links are found in inline links and images, reference definitions ([x]: target) and HTML href/src attributes. Code blocks, code spans and HTML comments are skipped, as are external URLs (https:, mailto: and other schemes), GitHub line anchors (#L10-L20) and anything that looks like a template placeholder.

Anchors follow GitHub's rules: the heading is rendered to plain text, lowercased, stripped of everything except letters, digits, -, _ and spaces, and each space becomes -. Repeated headings get -1, -2, ... suffixes, and links may use the user-content- prefix and percent-encoding. Headings inside list items and block quotes count too.

Bugs found with it

Development

python -m venv .venv && . .venv/bin/activate
pip install -e . pytest ruff
pytest
ruff check . && ruff format --check .

License

MIT

Metadata

Release files for mdlinkcheck 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mdlinkcheck 0.2.0
File Size Uploaded
mdlinkcheck-0.2.0.tar.gz 17.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mdlinkcheck 0.2.0
File Interpreter ABI Platform
mdlinkcheck-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 33.5 kB

Release files / mdlinkcheck-0.2.0.tar.gz

Download URL mdlinkcheck-0.2.0.tar.gz
Size 17.6 kB
Tags Source
SHA-256 checksum
How to use checksums
2e542b8b227d4c64978fd8f5b29f64787f9ae7548db2a837178e638b89bf39c6
BLAKE2b-256 checksum
How to use checksums
21b05f09909e43d763481b4f0eeab655914016aab83e13f8e3b82bdedd27c96d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / mdlinkcheck-0.2.0-py3-none-any.whl

Download URL mdlinkcheck-0.2.0-py3-none-any.whl
Size 15.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1dfd9bc35c3a6a07310817642608cc64212ebb85d6b7dc5d76804accd18d669c
BLAKE2b-256 checksum
How to use checksums
d0b7357e7114280b609c6df558fc9d2aa72eb4eaf2477490a133293bea290abc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page