pkms-lint
Find broken links in a Markdown vault. Wikilinks and reference-style ID links.
Point it at any folder of Markdown files. It reads [[Wikilinks]] the way Obsidian, Logseq, Foam and
Dendron write them, and it also reads the reference-style [text][KEY] form. It needs no index file,
no plugin, and no configuration. It never writes to your notes unless you pass --fix.
Install
pipx install pkms-lint
Or, inside a virtual environment:
pip install pkms-lint
Requires Python 3.11 or later. The only dependency is Typer.
Use
pkms-lint ~/my-vault # human-readable report
pkms-lint ~/my-vault --json # machine-readable report
pkms-lint ~/my-vault --fix # repair the two fixable kinds
pkms-lint ~/my-vault --fix --dry-run # show the repairs, write nothing
Example output:
pkms-lint 0.4.0 — 5 files — index: built by scan (no .pkms-index.json)
notes/index.md
2 BROKEN_WIKILINK Ghost note did you mean: notes/Ghost notes.md
3 AMBIGUOUS_WIKILINK Archive matches: notes/Archive.md, notes/old/Archive.md
Summary: 0 STALE_PATH · 0 MISSING_DEF · 0 UNKNOWN_ID · 0 UNDEFINED_REF ·
1 BROKEN_WIKILINK · 1 AMBIGUOUS_WIKILINK
5 notes carry no frontmatter block.
What it reports
| Code | Meaning | Example | Fixable |
|---|---|---|---|
BROKEN_WIKILINK |
The target matches no file | [[Ghost note]] and no such note exists |
no |
AMBIGUOUS_WIKILINK |
Two or more files match the target | [[Archive]] with notes/Archive.md and notes/old/Archive.md |
no |
UNDEFINED_REF |
An inline reference with no definition anywhere | [text][nope] and no [nope]: … line |
no |
STALE_PATH |
The definition points somewhere other than the indexed path | [K-0001]: old/note.md while the index says new/note.md |
yes |
MISSING_DEF |
An inline reference whose key is indexed but undefined in the file | [text][K-0001] with no [K-0001]: … line |
yes |
UNKNOWN_ID |
The key looks like an id but is absent from the index | [text][K-9999] with no such entry |
no |
A reference key that has a definition is correct Markdown and is never reported. [docs][python-docs]
with a matching [python-docs]: https://… line stays silent.
The index
pkms-lint needs a map from id to path only for the three reference-style codes. It finds one in three
ways, and the first line of every report says which:
| Line | Meaning |
|---|---|
index: built by scan |
No index file. Ids were read from the id: field in each note's frontmatter. |
index: .pkms-index.json |
An index file was found in the vault root. |
index: <path> |
You passed --manifest <path>. |
A vault with no ids at all works. The report ends with up to three count lines that describe it:
12 notes carry no frontmatter block.
3 notes carry frontmatter with no id: field.
2 duplicate ids (first file in sorted order kept).
These are counts, not problems. They never change the exit code. Most Markdown vaults have no ids
and that is fine — the wikilink checks do not need them. Add --show-duplicates to name each duplicate.
Notices
Two conditions are not problems, but you must know about them. pkms-lint reports each one as a notice
below the summary:
Notices (informational — these never change the exit code):
notes/index.md
1 CASE_MISMATCH archive differs only in case from: notes/Archive.md
4 UNSUPPORTED_SYNTAX ((0195f2a1-4b3c-7d8e-9f01-23456789abcd)) Logseq block reference — detected, not resolved
| Notice | Meaning |
|---|---|
CASE_MISMATCH |
The target matches no file with its case, but one file matches when case is ignored. A case-insensitive tool (Obsidian, macOS) opens it. A case-sensitive reader does not resolve this link. |
UNSUPPORTED_SYNTAX |
A Logseq block reference ((uuid)). It addresses a block inside a note, and pkms-lint does not model blocks. |
Notices are not findings. They never enter findings or counts, and they never change the exit code.
A target that matches two or more files when case is ignored is still an AMBIGUOUS_WIKILINK. The match
is case-sensitive, so Archive.md and archive.md are two different files, and [[Archive]] resolves to
the first one only.
What it does not do
- It never repairs a wikilink.
--fixtouchesSTALE_PATHandMISSING_DEFonly. - It does not validate a heading or a block id.
[[Note#Heading]]and[[Note#^abc]]are checked onNotealone. - It does not check non-Markdown embeds.
![[diagram.png]]is counted and skipped, never reported broken. - It does not score your vault. No orphan detection, no staleness, no connectivity score.
- It reads YAML frontmatter only. Logseq writes properties as
id:: <uuid>in the first block rather than in a---block, so a Logseq note counts under "no frontmatter block". That affects the count lines only; it never produces a finding. A Logseq block reference,((uuid)), is detected and reported as anUNSUPPORTED_SYNTAXnotice. It is never resolved.
Exit codes
| Code | Meaning |
|---|---|
| 0 | The run completed and found nothing to report |
| 1 | The run completed and reported at least one finding |
| 2 | Usage error: the vault is not a directory, or an explicit --manifest path does not exist |
Exit 1 on findings makes the tool usable in a pre-commit hook or a CI job. A notice never changes the exit code. A run with notices and no findings exits 0.
JSON output
--json emits one object:
{
"tool": "pkms-lint",
"version": "0.4.0",
"schema": 1,
"vault": "/home/you/my-vault",
"manifest_source": "scan",
"files_scanned": 5,
"assets_skipped": 0,
"notes_without_frontmatter": 5,
"notes_without_id": 0,
"duplicate_ids": 0,
"counts": { "BROKEN_WIKILINK": 1, "AMBIGUOUS_WIKILINK": 1 },
"findings": [
{
"failure": "BROKEN_WIKILINK",
"kind": "wikilink",
"file": "notes/index.md",
"line": 2,
"key": "Ghost note",
"old_path": null,
"new_path": null,
"candidates": [],
"suggestions": ["notes/Ghost notes.md"],
"embed": false,
"fixed": false
}
],
"notices": [
{
"notice": "CASE_MISMATCH",
"file": "notes/index.md",
"line": 4,
"key": "archive",
"candidates": ["notes/Archive.md"],
"detail": null
}
]
}
schema is an integer. It rises whenever a change to findings would break a reader that worked
before, so you can pin against it. Every finding carries all eleven keys; the ones a given failure does
not use are null or []. Every path is relative to the vault and uses / on every platform.
notices is always present. It is [] when the run has no notice. Each notice carries six keys:
notice, file, line, key, candidates and detail. detail is always null in this release.
Version 0.4.0 added the notices key and kept schema: 1, because no existing key changed.
Skipped folders
Any folder whose name starts with a dot, plus node_modules and logseq/bak. This is deliberate: a
note you deleted still exists inside .trash/, and Logseq keeps a backup of every edit under
logseq/bak/. Scanning either would let a deleted note resolve a link and report it as healthy.
License
MIT. See LICENSE.
About
pkms-lint is the first released module of PKMS, a set of local-first tools for maintaining a Markdown
vault readable by a human and by a language model alike. The other modules are not released yet.
Source and issues: https://github.com/gauthierae/PKMS
Metadata
Release files for pkms-lint 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pkms_lint-0.4.0.tar.gz | 25.8 kB | Without provenance | Details for pkms_lint-0.4.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pkms_lint-0.4.0-py3-none-any.whl | Python 3 | none | any | Details for pkms_lint-0.4.0-py3-none-any.whl |
Total release size: 38.7 kB
Release files / pkms_lint-0.4.0.tar.gz
| Download URL | pkms_lint-0.4.0.tar.gz |
|---|---|
| Size | 25.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
60d5e6f82247400ee1b327c37d98511c8311e7edcca16776d4c2ddae6c9c6ce0
|
|
BLAKE2b-256 checksum How to use checksums |
4a3dd19b8d68589aa046bb5d78afca801b29f9b7f3afacf2d4792e78f1da9e3f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / pkms_lint-0.4.0-py3-none-any.whl
| Download URL | pkms_lint-0.4.0-py3-none-any.whl |
|---|---|
| Size | 12.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cb34bd8e4ff94384396bc9dbbe2bd44925b70df520db189c9baec548c4b87b44
|
|
BLAKE2b-256 checksum How to use checksums |
09f97c68829d043f04fd1a4a430ea3448c27391b8b39c1dffb60071b6d07e793
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for pkms-lint 0.4.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.