Skip to main content

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. --fix touches STALE_PATH and MISSING_DEF only.
  • It does not validate a heading or a block id. [[Note#Heading]] and [[Note#^abc]] are checked on Note alone.
  • 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 an UNSUPPORTED_SYNTAX notice. 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)

Source distribution for pkms-lint 0.4.0
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)

Table of built distributions (wheels) for pkms-lint 0.4.0
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files No provenance

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