sinhasheshant / citable-markdown
Make specific lines, paragraphs, list items, tables and code blocks in markdown citable with short readable ^ids, cite them with links that carry a verifying quote, and check every reference with a script that reports dangling, stale and moved references instead of silently pointing at the wrong text. Use when writing or editing markdown that refers to other parts of itself or other files, when a person asks about "line 14" or "that paragraph", when reviewing docs for broken internal links, or when an agent must cite exactly which passage supports a claim.
Citable markdown
Plain markdown can only link to headings, heading links break when the heading
is renamed, and broken links fail silently. Line numbers drift on every edit.
This skill gives any block a short readable name, makes every citation carry
a quote that proves what it points at, and checks both with a script.
When to reach for this
You are writing a markdown document that refers back to its own parts
("as noted above", "see the failure table") or to passages in other.mdfiles.A person refers to a line or passage ("line 14 is wrong", "that paragraph
about retries") and you need to find it exactly, even if the file has changed.- You must cite which passage supports a claim, in a way someone can check.
You are reviewing or editing markdown docs and want to know which internal
links are broken or now point at the wrong text.
Not for: links to web pages (use normal URLs), or editing code files (use your
edit tool's exact-match replacement).
The convention
Anchor: make a block citable by giving it an id.
| Target | How |
|---|---|
| A paragraph, one list item, or one table row | end its last line with a space and ^id |
| A heading | nothing needed (GitHub-style slug: ## Retry plan is #retry-plan), or end it with ^id for a name that survives renames |
| A table, fenced code block, quote, or whole list | put ^id alone on the line after it, then a blank line |
Most OCR failures come from rotated pages. ^ocr-cause
| Case | Score |
|---|---|
| scan-07 | 4 |
^failure-tableIds: lowercase letters, digits and hyphens, short and meaningful
(ocr-cause, retry-plan). They must be unique within a file.
Reference: cite with a normal markdown link to a fragment, and put a short
quote from the target in the link title:
The [likely cause](#ocr-cause "rotated pages") matches the [table](#failure-table "scan-07 | 4").
In another file: [the plan](plan.md#retry-plan "rerun only scanned").
A line, when no anchor exists: [line 14](#L14 "exact words on that line").
A range: [lines 14 to 18](#L14-L18 "words from that range").The quote is what makes a reference checkable: an id says where, the quote
says what should be there. Matching ignores case, extra whitespace, and the* _ ` emphasis marks.
Process
Every step below reads or runs something. If a tool call fails (a file can't be
read, a command can't run), say so first in your answer, and never describe a
file you couldn't read or a step you couldn't do as if it were done.
Look before adding. List what is already citable in the files you'll
touch, so you reuse ids instead of inventing duplicates:python3 SKILL_DIR/scripts/check_refs.py --list path/to/file.mdSKILL_DIRis the directory holding this SKILL.md (for example.claude/skills/citable-markdown). Output is one line per target:file:line ^id "first words"for anchors,#slugfor headings.
Add--jsonfor the full text of each target.Add an anchor only where something cites it. Don't put ids on every
paragraph. When you write a citation, give its target an id if it has none,
following the table above. Choose a name from the content (^ocr-cause, not^p3or a random string).Write each reference with a quote. Copy 3 to 8 consecutive words
verbatim from the target, picking words that are distinctive to it. Open
the target and copy them; don't write the quote from memory. Always write
link text:[](#id)renders as nothing in most viewers.Prefer ids over line numbers. Use
#L14only for files you can't or
shouldn't add anchors to, or to record what a person told you. Always add a
quote to a line reference; without one it is only a guess.Resolve a person's line reference. When someone says "line 14 says X",
don't trust the number. Write or imagine it as[x](#L14 "X")and run the
checker. Arelocatedresult tells you where X is now. Astaleresult means
X isn't in the file (or appears more than once): ask the person, don't guess.Verify. Run the checker on every file you changed:
python3 SKILL_DIR/scripts/check_refs.py path/to/docs/Success looks like
OK: 12 references checked, all resolvedand exit code 0.
Otherwise each problem is printed asfile:line: severity code: message.
Fix each one:Code Meaning What to do dangling(error)no ^id, heading slug, file or line with that nameFix the typo, add the missing anchor, or remove the link stale(error)target exists but doesn't contain the quote, or a line quote is found 0 or 2+ times Read the target. If the text changed, decide whether the citation still holds; update the quote or retarget it. Never just delete the quote to silence it duplicate-id(error)the same ^idtwice in a fileRename one, then update the links that meant it orphan-anchor(error)a standalone ^idwith no block aboveMove it directly under its block relocated(warning)a line ref's quote is now on another line, found exactly once Update the line number, or run with --fixto rewrite itunverified(info)a line ref without a quote Add a quote, or replace it with an anchor Re-run until it prints
OK. Use--strictto make warnings andunverifiedfail too (useful in CI). Use--jsonto parse results.If the checker can't run (no Python, no shell, the command fails or
errors before printing results): try once more at most, then stop. Your
edits stand, but they are unchecked. Tell the user exactly that: which
files you changed, the references you added, what went wrong running the
checker, and the command for them to run:python3 SKILL_DIR/scripts/check_refs.py path/to/docs/. Don't keep retrying
and don't check the references by eye and call them verified.Renaming an id. Search for every use first, then change the anchor and
all references together, then re-run the checker:grep -rn -e '#old-id' -e '\^old-id' path/to/docs/
What not to do
Don't report work your tools didn't do. "I examined the workspace" after
every read failed is a false report, even when the user asked you to confirm
it is done. Say what failed and what is still to do.Don't say the references are verified unless the checker printed
OK.
"Done", "all references resolved" or "verified" after a checker that failed
or never ran is a false report. Say the edits are unchecked, and why.Don't cite a line number you haven't just read. Line numbers drift with
every edit above them, and models are unreliable at recalling them. Read the
file, then cite, with a quote.Don't treat "the link resolves" as "the citation is right". A
#idcan
still exist after its text changed. Only the quote check proves the target
still says what you claimed.Don't silence a
staleerror by deleting or loosening the quote. That
hides exactly the drift this exists to catch. Re-read the target and decide.Don't use random or hash ids (
^a8f3c2, UUIDs). People can't read,
remember, or type them, and they add clutter. Use names from the content.**Don't put
^idin the middle of a line, inside a code block, or inside
inline code.** It is only recognized at the end of a line, or alone on a line
after a block. Inside code it is ignored and links to it are reporteddangling.Don't anchor everything up front. Ids that nothing cites are noise. Add
them when you add the citation.Don't write empty link text
[](#id). It renders as nothing outside a
smart renderer.Don't run
--fixwithout reviewing the diff. It only rewrites line numbers
forrelocatedreferences, but check the new location is what was meant.
Limits
Heading slugs follow GitHub's rules approximately. If an anchor and a
heading slug share a name, the^idwins.On GitHub, links to
^idtargets render but don't scroll anywhere; only
heading links jump. The checker still validates them.Quotes are matched within the target block only, except for line references,
which fall back to a whole-file search to find where the text moved.