Skip to content
Workflow

Markdown docs with markdownlint and offline link checks

Agent-written READMEs and docs checked offline: markdownlint-cli2, relative links and #anchors resolve, and every npm run command in the docs exists.

Verified install Not agent-tested v1.0.0 MIT updated markdownnodejs
Equips
  • 1 1 Rules
  • 1 1 Skills
  • – 0 MCP

Agents write docs that read well and are quietly wrong. A link points to a page that was renamed. An anchor no longer matches its heading after an edit. A quick start says npm run deploy when there is no deploy script. This loadout gives your agent two checks that need no network. markdownlint-cli2 enforces style, with code block languages and in-file link fragments required. A small script checks that relative links and images resolve and that #fragments match GitHub-style heading slugs, across files too. It also checks that every npm, pnpm, yarn or bun run command in the docs names a real script. External URLs are left to a scheduled checker, so a flaky site never blocks a change. The verifier runs both on a clean docs tree and on a broken sample, with markdownlint-cli2 0.23.3.

What's in it

  • Relative links, images and #anchors checked offline, across files
  • GitHub heading slug rules, including repeated headings
  • Catches npm run commands in docs that have no script
  • markdownlint-cli2 with code block languages and link fragments enforced
  • Verified with markdownlint-cli2 0.23.3 on a clean tree and a broken sample

You'll need

  • Tools: node, npm
Level 1 · automated

Verification

Verified install

Checked at commit 08c3710 · 9 passed, 0 failed, 1 not applicable

CheckResultWhat we found
Listinglisting.yaml matches the schema and declares every file in the folder. Pass
Schema valid; 6 files declared, none missing or extra
  • tested.status: untested (agent tests are separate from Level 1)
Files parseJSON parses; SKILL.md and .mdc front matter is valid YAML with the fields each agent needs; code fences are closed. Pass All 6 files parse; front matter valid
AdaptersCLAUDE.md imports @AGENTS.md, GEMINI.md imports @./AGENTS.md, and both match the generator. Pass
CLAUDE.md and GEMINI.md import AGENTS.md and are up to date
  • CLAUDE.md: matches the generator
  • GEMINI.md: matches the generator
Install pathsEvery file lands where each agent documents it, with no collisions, and the laid-out project is re-discovered correctly. Pass
Correct paths for all 6 agents; layouts re-discovered
  • Claude Code: 5 files, 1 skill in .claude/skills/
  • Codex CLI: 4 files, 1 skill in .agents/skills/
  • Cursor: 4 files, 1 skill in .agents/skills/
  • Gemini CLI: 5 files, 1 skill in .agents/skills/
  • Antigravity CLI: 4 files, 1 skill in .agents/skills/
  • GitHub Copilot: 4 files, 1 skill in .agents/skills/
MCP configmcp.json is structurally valid, converts to each agent's format, declares no literal secrets, and referenced packages exist on npm/PyPI. Not applicable No MCP servers in this loadout
Safety scanThe intake scanner finds no credentials, risky shell, hidden Unicode or prompt-injection wording, and the listing's network flag is honest. Pass
Clean: no credentials, risky shell or hidden text
  • Scanner intake-precheck/1: verdict clean; network no (listing says yes)
ReferencesLinks and file paths between the loadout's files resolve, including for each agent's installed layout. Pass 1 reference between files resolves for every agent
Size limitsFile count and sizes are within the submission limits; SKILL.md bodies stay under 500 lines. Pass
6 files, 10.1 KB total; largest skills/docs-check/scripts/check_docs.mjs 5.0 KB
  • Limits: 30 files, 32 KB per file, 128 KB total, 500 lines per skill or rule
LicenceA recognised SPDX licence, with the licence text available. Pass MIT, by Agent Loadout; text in the repository LICENSE
Starter buildA minimal fixture project with the loadout applied installs and builds or type-checks, and the loadout's own code examples compile. Pass
The docs-check skill's markdownlint-cli2 config (0.23.3) and check_docs.mjs pass on a README and docs tree with cross-file anchors, an image and four npm script commands; a sample with a broken link, two wrong anchors, an invented npm script and an unlabelled code fence fails both tools.
  • Applied 9 loadout files at every agent's paths
  • ✓ npm ci (markdownlint-cli2 pinned): npm ci --no-audit --no-fund --ignore-scripts
  • ✓ place the markdownlint config: bash -c cp .agents/skills/docs-check/references/markdownlint-cli2.jsonc .markdownlint-cli2.jsonc && echo placed
  • ✓ markdownlint-cli2 is clean (README, docs, and the loadout's own AGENTS.md): npx markdownlint-cli2
  • ✓ check_docs.mjs is clean: npm run -s docs:check
  • ✓ the bad sample fails both: bash -c mkdir -p bad && cp samples/bad-readme.md.txt bad/README.md && npx markdownlint-cli2 'bad/**/*.md' 2>&1; a=$?; node .agents/skills/docs-check/scripts/check_docs.mjs bad; b=$?; test $a -ne 0 && test $b -eq 1 && echo both failed
  • Covers: Relative links and images resolve; #fragments match GitHub-style heading slugs in the same or another file
  • Covers: npm/pnpm/yarn/bun run commands in code blocks and inline code exist in the nearest package.json
  • Covers: markdownlint-cli2 with fenced-code languages (MD040) and link fragments (MD051) enforced

What this means: a script checked the files themselves: they parse, they install where each agent looks, any MCP config is sound, the safety scan is clean, references between files resolve, and, where a free and repeatable fixture exists, a small starter project builds with the loadout applied. No AI agent was run. Per-agent results ("tested with agent") are separate and stay untested until an agent actually runs.

Install

Choose your agent. Run the commands from your project root, or download a zip already laid out in the right folders.

Claude Code reads CLAUDE.md (imports AGENTS.md with @AGENTS.md) and skills from .claude/skills/.

Download zip
curl -fsSL --create-dirs -o AGENTS.md https://loadout.jnw-it.com/raw/docs-markdown-checks/1.0.0/AGENTS.md
curl -fsSL --create-dirs -o CLAUDE.md https://loadout.jnw-it.com/raw/docs-markdown-checks/1.0.0/CLAUDE.md
curl -fsSL --create-dirs -o .claude/skills/docs-check/SKILL.md https://loadout.jnw-it.com/raw/docs-markdown-checks/1.0.0/skills/docs-check/SKILL.md
curl -fsSL --create-dirs -o .claude/skills/docs-check/scripts/check_docs.mjs https://loadout.jnw-it.com/raw/docs-markdown-checks/1.0.0/skills/docs-check/scripts/check_docs.mjs
curl -fsSL --create-dirs -o .claude/skills/docs-check/references/markdownlint-cli2.jsonc https://loadout.jnw-it.com/raw/docs-markdown-checks/1.0.0/skills/docs-check/references/markdownlint-cli2.jsonc
FileGoes to
AGENTS.mdAGENTS.md
CLAUDE.mdCLAUDE.md
skills/docs-check/SKILL.md.claude/skills/docs-check/SKILL.md
skills/docs-check/scripts/check_docs.mjs.claude/skills/docs-check/scripts/check_docs.mjs
skills/docs-check/references/markdownlint-cli2.jsonc.claude/skills/docs-check/references/markdownlint-cli2.jsonc

Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want.

Files

Rules AGENTS.md Rules for Markdown docs whose links, anchors and commands are checked before a change is done. 22 lines
Download Raw
# AGENTS.md: Markdown docs that stay correct

This repository keeps its documentation in Markdown: the root README and pages under `docs/`. Docs are reviewed
like code: links, anchors and commands must be right, not just readable.

## Commands

- Style: `npx markdownlint-cli2` (config in `.markdownlint-cli2.jsonc`).
- Links, anchors and script commands: `node .agents/skills/docs-check/scripts/check_docs.mjs` (offline).
- Done means: both are clean.

## Rules

- Use the `docs-check` skill whenever you add or edit Markdown.
- Every command in the docs is one you ran or copied from `package.json`. Never invent a script, flag or
  environment variable.
- Fenced code blocks name their language.
- When a heading or file is renamed, fix every link to it in the same change.
- Relative links between docs pages. External links only to official or primary sources.
- Do not add markdownlint disable comments to get a clean run.
- Keep the README short: what it is, quick start, where the docs are. Detail goes in `docs/`.
Adapter CLAUDE.md Imports AGENTS.md for Claude Code. 11 lines
Download Raw
# CLAUDE.md

@AGENTS.md

## Claude Code notes

- Skills from this loadout live in `.claude/skills/`: `docs-check`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.

<!-- From the Agent Loadout "Markdown docs with markdownlint and offline link checks" v1.0.0 — https://loadout.jnw-it.com/loadouts/docs-markdown-checks/ -->
Adapter GEMINI.md Imports AGENTS.md for Gemini CLI. 11 lines
Download Raw
# GEMINI.md

@./AGENTS.md

## Gemini CLI notes

- Skills from this loadout live in `.agents/skills/`: `docs-check`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.

<!-- From the Agent Loadout "Markdown docs with markdownlint and offline link checks" v1.0.0 — https://loadout.jnw-it.com/loadouts/docs-markdown-checks/ -->
Skill skills/docs-check/SKILL.md Both checks, the config, and writing rules for docs. 54 lines
Download Raw
---
name: docs-check
description: Check Markdown docs before calling a docs change done - markdownlint-cli2 for style, and an offline check that relative links, images and #anchors resolve and that npm run commands in the docs exist. Use when writing or editing README files, docs/ pages, or anything that links between Markdown files.
---

# Docs check

Agents write docs that read well and are wrong in small ways: a link to a page that was renamed, an anchor
that no longer matches its heading, `npm run deploy` when there is no deploy script. Two checks catch these
without the network.

## Run both

```bash
npx markdownlint-cli2                                         # style; config in .markdownlint-cli2.jsonc
node .agents/skills/docs-check/scripts/check_docs.mjs         # links, anchors, script commands
```

`check_docs.mjs` takes paths (files or folders); with none it checks the root README and `docs/`.
Output is `file:line: problem` and a summary line; exit 1 means fix before finishing.

## What check_docs.mjs checks

- Relative links, images and reference definitions point at files that exist. A leading `/` means the
  repository root.
- A `#fragment` matches a heading in the target file, using GitHub's slug rules: lower case, punctuation
  dropped, spaces to hyphens, `-1`, `-2` for repeated headings. Explicit `<a id="...">` anchors count too.
- Every `npm run`, `pnpm run`, `yarn run` or `bun run` command in a code block or inline code names a script
  in the nearest `package.json`.
- External URLs are not fetched. Leave those to a scheduled link checker so a flaky site never blocks a change.

## Markdownlint config

Start from `references/markdownlint-cli2.jsonc` (copy it to the repository root as
`.markdownlint-cli2.jsonc`). It keeps the defaults and adds:

- MD040: every fenced block names its language (`bash`, `json`, `text` ...), so readers and highlighters know
  what it is.
- MD051: link fragments in the same file must match a heading.
- MD013 off (no hard wrapping of prose), MD024 only for sibling headings, a small HTML allow-list.

## Writing rules

- Commands in docs are commands you ran. Copy them from `package.json` scripts or your terminal, not memory.
- When you rename a heading or move a file, search for links to it (`rg -n 'old-slug|old-file.md'`) and fix
  them in the same change.
- Link with relative paths between docs files, never to the rendered GitHub URL of the same repository.
- Do not disable a markdownlint rule inline to get a clean run. Fix the Markdown, or change the shared config
  with a reason in the pull request.

## Done means

Both commands are clean, and the summary line from `check_docs.mjs` is in your report.
Skill file skills/docs-check/scripts/check_docs.mjs Offline link, anchor and script-command check. 122 lines
Download Raw
#!/usr/bin/env node
// Offline docs check for Markdown: relative links and images resolve, #fragments match a heading
// (GitHub-style slugs, including in other files), and `npm run <script>` style commands exist in package.json.
// No network: external URLs are left to a scheduled link checker.
//
//   node check_docs.mjs [paths...]     default: README.md and docs/ (when present)
// Exit 0 when clean, 1 when there are problems.
import fs from "node:fs";
import path from "node:path";

const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", ".venv"]);
const args = process.argv.slice(2);
const roots = args.length ? args : ["README.md", "docs"].filter((p) => fs.existsSync(p));

function walk(p, out) {
  const st = fs.statSync(p);
  if (st.isDirectory()) {
    for (const e of fs.readdirSync(p)) if (!SKIP_DIRS.has(e)) walk(path.join(p, e), out);
  } else if (/\.mdx?$/i.test(p)) out.push(path.normalize(p));
  return out;
}
const files = roots.flatMap((r) => (fs.existsSync(r) ? walk(r, []) : (console.log(`${r}: not found`), process.exitCode = 1, [])));

// Strip inline Markdown from a heading, then slug it the way GitHub does.
function slug(text) {
  return text
    .replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
    .replace(/`([^`]*)`/g, "$1")
    .replace(/<[^>]+>/g, "")
    .replace(/[*_~]{1,3}(\S[^*_~]*?)[*_~]{1,3}/g, "$1")
    .trim()
    .toLowerCase()
    .replace(/[^\p{L}\p{N}\p{M}\s_-]/gu, "")
    .replace(/\s/g, "-");
}

// Lines outside fenced code blocks, plus the fenced blocks themselves (for command checks).
function parse(file) {
  const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
  const prose = [];
  const code = [];
  let fence = null;
  lines.forEach((line, i) => {
    const m = line.match(/^\s{0,3}(`{3,}|~{3,})/);
    if (m && !fence) { fence = m[1]; return; }
    if (fence && line.trim().startsWith(fence)) { fence = null; return; }
    (fence ? code : prose).push({ n: i + 1, text: line });
  });
  return { prose, code };
}

const anchorCache = new Map();
function anchors(file) {
  if (anchorCache.has(file)) return anchorCache.get(file);
  const set = new Set();
  const seen = new Map();
  for (const { text } of parse(file).prose) {
    const h = text.match(/^\s{0,3}#{1,6}\s+(.*?)\s*#*\s*$/);
    for (const m of text.matchAll(/<a\s+(?:name|id)="([^"]+)"/g)) set.add(m[1]);
    if (!h) continue;
    const base = slug(h[1]);
    const count = seen.get(base) ?? 0;
    seen.set(base, count + 1);
    set.add(count ? `${base}-${count}` : base);
  }
  anchorCache.set(file, set);
  return set;
}

function nearestPackageJson(file) {
  let dir = path.resolve(path.dirname(file));
  for (;;) {
    const p = path.join(dir, "package.json");
    if (fs.existsSync(p)) return p;
    const up = path.dirname(dir);
    if (up === dir) return null;
    dir = up;
  }
}

const problems = [];
let linksChecked = 0;
let commandsChecked = 0;
const report = (file, n, msg) => problems.push(`${file}:${n}: ${msg}`);

for (const file of files) {
  const { prose, code } = parse(file);
  for (const { n, text } of prose) {
    const noCode = text.replace(/`[^`]*`/g, "");
    const targets = [
      ...[...noCode.matchAll(/!?\[[^\]]*\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)/g)].map((m) => m[1]),
      ...[...noCode.matchAll(/^\s{0,3}\[[^\]]+\]:\s*<?(\S+?)>?(?:\s+.*)?$/g)].map((m) => m[1]),
      ...[...noCode.matchAll(/<(?:img|a)\s[^>]*(?:src|href)="([^"]+)"/g)].map((m) => m[1]),
    ];
    for (const t of targets) {
      if (/^[a-z][a-z0-9+.-]*:/i.test(t) || t.startsWith("//")) continue; // external, mailto:, etc.
      linksChecked++;
      const [rawPath, frag] = t.split("#");
      const decoded = decodeURIComponent(rawPath);
      const target = decoded === "" ? file : decoded.startsWith("/") ? path.normalize(decoded.slice(1)) : path.normalize(path.join(path.dirname(file), decoded));
      if (!fs.existsSync(target)) { report(file, n, `broken link: ${t} (${target} does not exist)`); continue; }
      if (frag !== undefined && /\.mdx?$/i.test(target) && !anchors(target).has(decodeURIComponent(frag).toLowerCase())) {
        report(file, n, `missing anchor: ${t} (no heading "#${frag}" in ${target})`);
      }
    }
  }
  const pkgPath = nearestPackageJson(file);
  const scripts = pkgPath ? Object.keys(JSON.parse(fs.readFileSync(pkgPath, "utf8")).scripts ?? {}) : null;
  const inlineCode = prose.flatMap(({ n, text }) => [...text.matchAll(/`([^`]+)`/g)].map((m) => ({ n, text: m[1] })));
  for (const { n, text } of [...code, ...inlineCode]) {
    for (const m of text.matchAll(/\b(npm|pnpm|yarn|bun)\s+run\s+([\w:.-]+)/g)) {
      commandsChecked++;
      if (!scripts) report(file, n, `"${m[0]}" but no package.json above ${file}`);
      else if (!scripts.includes(m[2])) report(file, n, `"${m[0]}": no "${m[2]}" script in ${path.relative(".", pkgPath)}`);
    }
  }
}

for (const p of problems) console.log(p);
console.log(`docs: ${files.length} files, ${linksChecked} local links, ${commandsChecked} script commands, ${problems.length} problems`);
if (problems.length) process.exitCode = 1;
Skill file skills/docs-check/references/markdownlint-cli2.jsonc Repository markdownlint-cli2 config. 19 lines
Download Raw
// .markdownlint-cli2.jsonc at the repository root. Run: npx markdownlint-cli2
{
  "config": {
    "default": true,
    // Long lines are fine in prose; wrapping makes diffs noisy.
    "MD013": false,
    // Same heading text is fine under different parents (e.g. "Usage" per command).
    "MD024": { "siblings_only": true },
    // Allow the small amount of HTML GitHub renders well.
    "MD033": { "allowed_elements": ["details", "summary", "kbd", "br", "img", "a"] },
    // Fenced code blocks must say their language.
    "MD040": true,
    // Link fragments must match a heading in the same file.
    "MD051": true
  },
  "globs": ["**/*.md"],
  "ignores": ["node_modules/**", "**/node_modules/**", "CHANGELOG.md", "dist/**"]
}

All files: source zip · Licence MIT · By Agent Loadout · First listed

Credits

Public work that inspired this loadout. The wording here is our own; follow the links for the originals.

Installing and testing

How do I install Markdown docs with markdownlint and offline link checks in Claude Code?

From your project root, put AGENTS.md, CLAUDE.md, skills/docs-check/SKILL.md at .claude/skills/docs-check/SKILL.md, skills/docs-check/scripts/check_docs.mjs at .claude/skills/docs-check/scripts/check_docs.mjs, skills/docs-check/references/markdownlint-cli2.jsonc at .claude/skills/docs-check/references/markdownlint-cli2.jsonc. The "Claude Code" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/docs-markdown-checks/claude-code.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Markdown docs with markdownlint and offline link checks in Codex CLI?

From your project root, put AGENTS.md, skills/docs-check/SKILL.md at .agents/skills/docs-check/SKILL.md, skills/docs-check/scripts/check_docs.mjs at .agents/skills/docs-check/scripts/check_docs.mjs, skills/docs-check/references/markdownlint-cli2.jsonc at .agents/skills/docs-check/references/markdownlint-cli2.jsonc. The "Codex CLI" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/docs-markdown-checks/codex.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Markdown docs with markdownlint and offline link checks in Cursor?

From your project root, put AGENTS.md, skills/docs-check/SKILL.md at .agents/skills/docs-check/SKILL.md, skills/docs-check/scripts/check_docs.mjs at .agents/skills/docs-check/scripts/check_docs.mjs, skills/docs-check/references/markdownlint-cli2.jsonc at .agents/skills/docs-check/references/markdownlint-cli2.jsonc. The "Cursor" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/docs-markdown-checks/cursor.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Markdown docs with markdownlint and offline link checks in Gemini CLI?

From your project root, put AGENTS.md, GEMINI.md, skills/docs-check/SKILL.md at .agents/skills/docs-check/SKILL.md, skills/docs-check/scripts/check_docs.mjs at .agents/skills/docs-check/scripts/check_docs.mjs, skills/docs-check/references/markdownlint-cli2.jsonc at .agents/skills/docs-check/references/markdownlint-cli2.jsonc. The "Gemini CLI" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/docs-markdown-checks/gemini-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Markdown docs with markdownlint and offline link checks in Antigravity CLI?

From your project root, put AGENTS.md, skills/docs-check/SKILL.md at .agents/skills/docs-check/SKILL.md, skills/docs-check/scripts/check_docs.mjs at .agents/skills/docs-check/scripts/check_docs.mjs, skills/docs-check/references/markdownlint-cli2.jsonc at .agents/skills/docs-check/references/markdownlint-cli2.jsonc. The "Antigravity CLI" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/docs-markdown-checks/antigravity-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Markdown docs with markdownlint and offline link checks in GitHub Copilot?

From your project root, put AGENTS.md, skills/docs-check/SKILL.md at .agents/skills/docs-check/SKILL.md, skills/docs-check/scripts/check_docs.mjs at .agents/skills/docs-check/scripts/check_docs.mjs, skills/docs-check/references/markdownlint-cli2.jsonc at .agents/skills/docs-check/references/markdownlint-cli2.jsonc. The "GitHub Copilot" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/docs-markdown-checks/copilot.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

Has Markdown docs with markdownlint and offline link checks been tested?

It passed the Level 1 automated install checks on 2026-10-17: the files parse, install to each agent's documented paths, and pass the safety scan. No AI agent has run it yet, so no agent pass or fail is claimed.

More for Claude Code, Codex, Cursor, Gemini CLI, Antigravity CLI and GitHub Copilot.