A linter for Obsidian Flavored Markdown files
Project description
mdlint-obsidian
A Python library and CLI tool that lints Obsidian Flavored Markdown files.
It checks for structural problems — unclosed wikilinks, invalid frontmatter, malformed tables, unmatched math delimiters and more — so you catch issues before they cause broken renders in your vault.
Installation
pip install mdlint-obsidian
Or with pipx for an isolated CLI install:
pipx install mdlint-obsidian
Usage
CLI
# Lint a single file
mdlint path/to/note.md
# Lint an entire vault (all .md files recursively)
mdlint path/to/vault/
# Enable broken-link checking (requires vault root)
mdlint note.md --vault path/to/vault/
# Show only errors (suppress warnings)
mdlint note.md --severity error
# Machine-readable JSON output
mdlint note.md --format json
Exit codes: 0 if no errors (warnings alone do not fail), 1 if any errors are found.
Example output (text format):
notes/my-note.md:5: [ERROR] unclosed-wikilink: Wikilink [[ is not closed with ]]
notes/my-note.md:12: [WARNING] broken-link: Link [[Missing Note]] does not resolve to an existing note
Example output (JSON format):
[
{
"file": "notes/my-note.md",
"line": 5,
"rule": "unclosed-wikilink",
"severity": "error",
"message": "Wikilink [[ is not closed with ]]"
}
]
Python library
from mdlint_obsidian import validate, LintError, Severity
content = open("my-note.md").read()
# Basic validation
errors = validate(content)
# With vault path for broken-link checking
errors = validate(content, vault_path="/path/to/vault")
for error in errors:
print(f"Line {error.line} [{error.severity.value.upper()}] {error.rule}: {error.message}")
The validate() function returns a list of LintError dataclass instances:
@dataclass
class LintError:
rule: str # e.g. "unclosed-wikilink"
severity: Severity # Severity.ERROR or Severity.WARNING
line: int # 1-indexed line number
message: str
Filtering by severity:
from mdlint_obsidian import validate, Severity
errors = validate(content)
errors_only = [e for e in errors if e.severity == Severity.ERROR]
warnings_only = [e for e in errors if e.severity == Severity.WARNING]
Rules
All rules skip content inside fenced code blocks (` `` or ~~~).
Frontmatter
| Rule | Severity | Description |
|---|---|---|
frontmatter-not-first |
ERROR | A ---...--- block that parses as YAML frontmatter exists but does not start at line 1 |
frontmatter-invalid-yaml |
ERROR | The frontmatter block cannot be parsed as valid YAML |
frontmatter-unclosed |
ERROR | An opening --- at line 1 has no closing --- |
Wikilinks
| Rule | Severity | Description |
|---|---|---|
unclosed-wikilink |
ERROR | [[ without a matching ]] |
empty-wikilink |
ERROR | [[]] with no content |
wikilink-invalid-chars |
ERROR | Wikilink target contains # or ^ in invalid positions (e.g. multiple #, or ^ before #) |
broken-link |
WARNING | [[Note Name]] does not resolve to an existing .md file in the vault (only when --vault is provided) |
Embeds
| Rule | Severity | Description |
|---|---|---|
unclosed-embed |
ERROR | ![[ without a matching ]] |
empty-embed |
ERROR | ![[]] with no content |
embed-invalid-dimension |
ERROR | ![[file|WxH]] where the dimension suffix is malformed (e.g. 300x or x200) |
Callouts
| Rule | Severity | Description |
|---|---|---|
callout-invalid-type |
WARNING | > [!type] where type is not one of the 13 built-in Obsidian types or their aliases (custom CSS types are valid, hence warning) |
callout-missing-continuation |
ERROR | The line immediately after a callout header is non-empty and does not start with > |
callout-invalid-modifier |
ERROR | The modifier after the callout type is not + or - |
Built-in callout types: note, abstract/summary/tldr, info, todo, tip/hint/important, success/check/done, question/help/faq, warning/caution/attention, failure/fail/missing, danger/error, bug, example, quote/cite
Code Blocks
| Rule | Severity | Description |
|---|---|---|
unclosed-code-block |
ERROR | An opening fence (``` or ~~~) has no matching closing fence |
Formatting
| Rule | Severity | Description |
|---|---|---|
unclosed-highlight |
ERROR | == opened but not closed on the same line |
unclosed-comment |
ERROR | %% opened but never closed (document-level) |
Footnotes
| Rule | Severity | Description |
|---|---|---|
orphaned-footnote-ref |
ERROR | [^id] reference in the body with no matching [^id]: definition |
orphaned-footnote-def |
ERROR | [^id]: definition with no matching [^id] reference in the body |
Tables
| Rule | Severity | Description |
|---|---|---|
table-missing-separator |
ERROR | Table header row is not followed by a separator row (`|--- |
table-inconsistent-columns |
ERROR | A table body row has a different column count than the header |
Math
| Rule | Severity | Description |
|---|---|---|
unclosed-math-block |
ERROR | $$ block opened but never closed (document-level) |
unclosed-inline-math |
WARNING | $ opened but not closed on the same line (conservative: ignores $100-style currency) |
Development
git clone https://github.com/codeafix/mdlint-obsidian.git
cd mdlint-obsidian
pip install -e ".[dev]"
pytest
pytest --cov=mdlint_obsidian --cov-report=term-missing
See CONTRIBUTING.md for information on adding new rules.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mdlint_obsidian-0.2.6.tar.gz.
File metadata
- Download URL: mdlint_obsidian-0.2.6.tar.gz
- Upload date:
- Size: 25.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d22f352f06d3ae45f9b0e79fe16fc9414279b811fa26a0c06e8b62538b3970fb
|
|
| MD5 |
9b65b7ddb45149fc74492623ae4d5dfd
|
|
| BLAKE2b-256 |
5305ece5c6ac1c76101a4fdff36f7f2bdbb1f589c107b3757b521f91f19ad469
|
Provenance
The following attestation bundles were made for mdlint_obsidian-0.2.6.tar.gz:
Publisher:
python-publish.yml on codeafix/mdlint-obsidian
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mdlint_obsidian-0.2.6.tar.gz -
Subject digest:
d22f352f06d3ae45f9b0e79fe16fc9414279b811fa26a0c06e8b62538b3970fb - Sigstore transparency entry: 1019754573
- Sigstore integration time:
-
Permalink:
codeafix/mdlint-obsidian@a4feead660f249dc8093d05773494bbe8a9d33d1 -
Branch / Tag:
refs/tags/v0.2.6 - Owner: https://github.com/codeafix
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@a4feead660f249dc8093d05773494bbe8a9d33d1 -
Trigger Event:
release
-
Statement type:
File details
Details for the file mdlint_obsidian-0.2.6-py3-none-any.whl.
File metadata
- Download URL: mdlint_obsidian-0.2.6-py3-none-any.whl
- Upload date:
- Size: 23.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
983ae246324f8c06964bb314f91af53aa7210bbaab2f8b20f7f48d3eee910511
|
|
| MD5 |
b5e2b68dddd00f431bf70553ff050d42
|
|
| BLAKE2b-256 |
73b638fc79b263bf3681e5116089fdcb69deebd3e6b267c702a73e56f1ec2230
|
Provenance
The following attestation bundles were made for mdlint_obsidian-0.2.6-py3-none-any.whl:
Publisher:
python-publish.yml on codeafix/mdlint-obsidian
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mdlint_obsidian-0.2.6-py3-none-any.whl -
Subject digest:
983ae246324f8c06964bb314f91af53aa7210bbaab2f8b20f7f48d3eee910511 - Sigstore transparency entry: 1019754718
- Sigstore integration time:
-
Permalink:
codeafix/mdlint-obsidian@a4feead660f249dc8093d05773494bbe8a9d33d1 -
Branch / Tag:
refs/tags/v0.2.6 - Owner: https://github.com/codeafix
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@a4feead660f249dc8093d05773494bbe8a9d33d1 -
Trigger Event:
release
-
Statement type: