Pro

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:

  1. Fabricated paths. The agent invents ./api/v2/handlers.md because it sounded right.
  2. Stale anchors. A heading rename in one section silently breaks [link](#old-anchor) somewhere else.
  3. Risky shell. A copied-from-the-internet snippet ends with curl https://… | sudo bash.
  4. 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 (![](./diagrams/foo.png)) 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:

PatternSeverity
sudo rm -rf /danger
rm -rf / variantsdanger
Piping curl/wget into bash/shdanger
Fork bombs (:(){ :|:& };:)danger
Raw dd if=… of=/dev/sd…danger
> /dev/sd… redirects to a block devicedanger
mkfs on a block devicedanger
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 languagealways, never, guaranteed, proven, the only, impossible, completely, infinite, zero, fastest, slowest, best.
  • Numeric or percent claims10×, 100%, 2x faster.
  • Year claimssince 2019, by 2024, during 2023.
  • Bounded-time claimsunder 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 #anchor links are checked.
  • Image paths. ![](./diagrams/foo.png) 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