Verification — catch what AI gets wrong in markdown
Deterministic checks for fabricated paths, broken anchors, risky shell commands, and over-confident claims. Pure regex and structure rules. Zero LLM calls.
Last updated
Every time you let an AI write your CLAUDE.md, AGENTS.md, or an
RFC, you accept four common failure modes:
- Fabricated paths. The agent invents
./api/v2/handlers.mdbecause it sounded right. - Stale anchors. A heading rename in one section silently
breaks
[link](#old-anchor)somewhere else. - Risky shell. A copied-from-the-internet snippet ends with
curl https://… | sudo bash. - Over-confident claims. “10× faster than X.” “Always works.” “Zero overhead.”
Verification flags all four, deterministically, with zero LLM
calls. Toggle it with Ctrl+Shift+V.
The state persists per-window via localStorage["mdview.verify-on"],
so once you turn it on, it stays on until you toggle off.
The four core rules
Fabricated paths
The verification engine walks the document for inline-code spans
that look like a path: at least one slash, no whitespace, no glob
characters, no URL prefix. So `src/utils/foo.ts`,
`docs/spec.md`, `tests/verify/paths.test.js` all become
candidates. Plain prose, fenced code blocks, and [markdown](./links)
are deliberately excluded — they produce too many false positives.
Every candidate gets batched into a single Rust filesystem call that canonicalizes the path against the document’s root. Anything that doesn’t resolve gets flagged red inline and listed in the sidebar:
The handler lives in `src/api/v2/handlers.ts`.
If src/api/v2/ doesn’t exist on disk, the inline code span
flashes red and the sidebar shows:
fabricated_path L42 Path not found: src/api/v2/handlers.ts
For documents opened from stdin (cat foo.md | mdview) there’s no
filesystem root to check against, so the rule is intentionally
disabled and an info-level “Path checks disabled” finding appears
in the sidebar to make that obvious.
Broken anchors (intra-document)
Internal [link](#anchor) references get checked against the
heading slugs the renderer actually generates. If you rename a
heading and forget to update its links, the anchor lookup fails:
## Background
…
See [the next section](#design) for the API.
## Architecture
The link to #design flashes red because no heading has slug
design — the renamed heading produces slug architecture.
The slug algorithm is shared with the renderer (the same module
both sides import) so the anchor check can never drift from what
ends up in the rendered HTML.
This rule covers same-document anchors only. Cross-document
links ([link](./other-file.md)) and image paths
() are not currently checked.
Risky shell commands
Inside fenced code blocks tagged bash, sh, zsh, powershell,
pwsh, cmd, console, or shell, ten patterns get flagged:
| Pattern | Severity |
|---|---|
sudo rm -rf / | danger |
rm -rf / variants | danger |
Piping curl/wget into bash/sh | danger |
Fork bombs (:(){ :|:& };:) | danger |
Raw dd if=… of=/dev/sd… | danger |
> /dev/sd… redirects to a block device | danger |
mkfs on a block device | danger |
Windows format C: | danger |
Windows mass-delete (del /f /s /q C:\) | danger |
chmod -R 777 / | warning |
The list is curated and intentionally short — the policy is “only patterns we’d bet a beer are destructive in 99% of contexts.” Any new addition has to come at the cost of removing a weaker one. False positives matter more than coverage here.
Over-confident claims
Sentences with absolute language or numeric claims get an info-level flag — not because they’re wrong, but because they’re the kind of statement a human reviewer should look at twice. The rule fires on:
- Strong language —
always,never,guaranteed,proven,the only,impossible,completely,infinite,zero,fastest,slowest,best. - Numeric or percent claims —
10×,100%,2x faster. - Year claims —
since 2019,by 2024,during 2023. - Bounded-time claims —
under 50ms,in under 3 seconds,under 2 minutes.
Each finding is severity info. The verifier never blocks on
these — they’re a “look closer here” prompt, not an error.
Block density (claims_dense)
To avoid drowning a paragraph that’s deliberately quantified, each
block caps individual findings at three. Any further matches in
the same block collapse into a single claims_dense info finding
saying “N more claim-shaped phrases in this block.”
Structural checks
While the four rules above target AI failure modes specifically, the engine also runs four structural checks that catch the kind of rot that happens when a doc is edited by anyone (human or AI) over time.
Heading-level skips (heading_skip)
If your heading levels jump from H2 to H4 with no H3 in between, the rule warns. Skips usually mean the doc was edited piecewise and a heading got demoted without its children moving with it.
Duplicate headings (duplicate_heading)
Two H2s with the same text in the same document get an info-level finding. This is informational because it’s sometimes deliberate (e.g. a recurring “Notes” subhead) and sometimes a copy-paste accident — the rule surfaces the case so you can decide.
Empty sections (empty_section)
A heading immediately followed by the next heading, with nothing between them, gets flagged. Usually a leftover from outline-first drafting that never got filled in.
Oversized document (oversized)
Documents over 500 headings or 50,000 characters trigger an info-level finding. Not a problem in itself — but past those thresholds, the document is probably better as a workspace of several files.
Findings sidebar
When the layer is on, a sidebar lists every finding grouped by severity:
- Danger (red) — risky-shell danger patterns.
- Warning (orange) — fabricated paths, broken anchors, heading skips, the chmod 777 case.
- Info (gray) — claims, duplicate headings, empty sections, oversize, claims_dense, path-check-disabled.
Click any finding to scroll to its location in the document; the target paragraph flashes briefly so you can see what was flagged.
The findings cache is LRU-bounded (16 documents) so toggling verification off and back on, or switching between recent docs, re-renders findings without re-running the full pipeline.
Why this exists
When AI coding agents rewrite documentation, the four core failure modes — fabricated paths, stale anchors, risky shell, over- confident claims — happen often. All four are detectable deterministically. None of them needs an LLM. Verification is the cheap, fast, offline pass that catches them before you trust the rest of the document.
The fact that it’s deterministic matters. The same document yields the same findings every time, regardless of the day, the model version, or the network. That’s the property an LLM-based linter can’t give you — and it’s exactly what you want when you’re using verification to build trust in your AI workflow.
What it does NOT do
So you know the boundaries:
- Cross-document links.
[link](./other-file.md)is not resolved. Only same-document#anchorlinks are checked. - Image paths.
is not checked for existence. (Falls under cross-document links above.) - Front-matter validation. YAML front-matter is parsed for the metadata card, but the verification layer does not validate it against any schema. There is no schema config and no required- fields check.
- External link liveness. No HTTP check on
https://links. - Spelling / grammar / style. Use a writing assistant for those.
- Auto-fix. Findings surface; you fix them in your editor.
Coming in MD View 1.0
The CLI version of verification — mdview verify FILE,
--strict, --json output, exit code 3 when findings are present
— is specified in web/doc/spec/cli-v1.md and ships with MD View
1.0. The 2.5.x line you’re currently running has the GUI overlay
only; toggle it with
Ctrl+Shift+V.
Once 1.0 ships, you’ll be able to script the same checks into CI:
# Coming in MD View 1.0
mdview verify CLAUDE.md
mdview verify CLAUDE.md --strict
mdview verify CLAUDE.md --json | jq '.findings[] | select(.level == "error")'
See also
- Git-diff overlay — what changed in the document.
- Reading CLAUDE.md / AGENTS.md — why this layer exists.
- Pricing — $24 lifetime for Pro.