Lint Rules
Reference catalogue of every built-in Panache lint rule, the diagnostic codes each rule emits, severity, auto-fix availability, and configuration requirements.
This page documents every built-in lint rule that Panache ships with. Each section lists the rule’s configuration name (used in [lint.rules]), the diagnostic codes the rule may emit at runtime, severity, auto-fix support, and any extension or metadata requirements.
For a user-friendly introduction to the linter, CLI usage, and configuration, see the Linting guide.
Diagnostic format
Diagnostics are displayed in a compiler-style format:
severity[diagnostic-code]: message
--> file:line:column
Components:
severity-
error,warning, orinfo diagnostic-code-
The specific code emitted (e.g.,
undefined-reference-label). A single rule may emit several distinct codes. message- Human-readable description of the issue
location- File path, line number, and column number
Some diagnostics include additional notes pointing at related locations:
warning[duplicate-reference-labels]: Duplicate reference definition 'link1'
--> document.qmd:4:1
note: First defined here:
--> document.qmd:3:1
Severity levels
Panache uses three severity levels:
error- Critical issues that prevent correct parsing or rendering
warning- Likely mistakes or best practice violations
info- Informational messages (currently unused, reserved for future use)
Rules
heading-hierarchy
Detects skipped heading levels that violate document structure best practices.
- Severity
- Warning
- Auto-fix
- Yes
- Diagnostic codes
-
heading-hierarchy - Description
- Headings should increment by at most one level (e.g., H1 → H2 → H3). Skipping levels (H1 → H3) makes the document structure unclear and can break table-of-contents generation.
Example violation:
# Main Title
### SubsectionDiagnostic:
warning[heading-hierarchy]: Heading level skipped from h1 to h3; expected h2
--> document.qmd:3:1
|
3 | ### Subsection
| ^^^^^^^^^^^^^^
Auto-fix: Changes ### Subsection to ## Subsection.
empty-list-item
Detects list items whose content is empty — a bare marker with nothing after it.
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
empty-list-item - Description
-
An empty list item is almost always a placeholder the author forgot to fill in. The rule fires in two situations:
- A
LIST_ITEMproduced by the parser has no inline content (e.g. a bare-line between two non-empty bullets, or1.with nothing after the period). - A bare
-on the line below a list item gets interpreted as a Setext H2 underline, silently merging the item with the previous line as a heading. (=underlines are not flagged because they don’t share the bullet-marker shape.)
Both cases are valid Markdown, but they tend to surprise readers.
- A
Example violation:
- Item one
-
- Item threeDiagnostic:
warning[empty-list-item]: List item has no content
--> document.md:2:1
|
2 | -
| ^
Resolution: Fill in the missing content, or delete the marker. No auto-fix is provided because the right choice (placeholder vs. removal) is an author-intent decision.
heading-eaten-attrs
Detects HTML comments that cause pandoc to silently drop a heading’s {...} attribute block.
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
heading-eaten-attrs - Requirements
-
extensions.header-attributes = true(the default for Pandoc, Quarto, and R Markdown flavors). - Description
-
Pandoc requires the
{...}attribute block on a heading to be followed only by whitespace. When any non-whitespace content (including an HTML comment) follows the brace block, pandoc treats the braces as literal text, silently dropping the attributes and producing an auto-generated id like"title-.unnumbered". Cross-references that targeted the intended id then break with no diagnostic from pandoc itself.
Example violation:
# Bibliography {.unnumbered} <!-- TODO -->Diagnostic:
warning[heading-eaten-attrs]: Comment on a heading line with `{...}` attributes;
pandoc treats the brace block as literal text when anything follows it on the line.
--> document.qmd:1:30
|
1 | # Bibliography {.unnumbered} <!-- TODO -->
| ^^^^^^^^^^^^^
help: Move the comment to its own line before or after the heading.
Resolution: Move the comment to its own line. No auto-fix is provided because the right placement (above or below the heading, or deleting the comment entirely) is an author-intent decision.
heading-strip-comments-residue
Detects HTML comments adjacent to a heading’s {...} attribute block that would leave stray whitespace under pandoc --strip-comments.
- Severity
- Warning
- Default
-
Off — opt in via
[lint.rules] heading-strip-comments-residue = true. - Auto-fix
- No
- Diagnostic codes
-
heading-strip-comments-residue - Requirements
-
extensions.header-attributes = true. - Description
-
When attributes still parse (the comment sits before the brace block), invoking pandoc with
--strip-commentsremoves the comment text but leaves the surrounding whitespace. The resulting heading source has trailing or interior whitespace adjacent to the attribute block, which can subtly affect downstream tooling. This rule is opt-in because most authors do not use--strip-comments; enable it when your publishing pipeline does.
Example violation:
# Bibliography <!-- TODO --> {.unnumbered}Resolution: Move the comment to its own line before or after the heading.
duplicate-reference-labels
Detects duplicate reference link and footnote definitions.
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
duplicate-reference-labels - Description
- Each reference label and footnote ID must be unique within a document. Duplicate definitions cause ambiguity—only the first definition is used, making the others ineffective.
Example violation:
See [link1] and [link2].
[link1]: https://example.com
[link1]: https://different.comDiagnostic:
warning[duplicate-reference-labels]: Duplicate reference definition 'link1'
--> document.qmd:4:1
note: First defined here:
--> document.qmd:3:1
Resolution: Rename or remove the duplicate definition.
undefined-references
Detects reference links and footnotes that point to missing definitions.
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
undefined-reference-label,undefined-footnote-id - Description
- Flags unresolved reference-style links (including shortcut/collapsed forms) and unresolved footnote references. This helps catch broken cross-references early in editing and CI.
Example violation:
See [missing][nope] and note[^missing].
[ok]: https://example.comDiagnostic:
warning[undefined-reference-label]: Reference label '[nope]' not found
--> document.qmd:1:15
warning[undefined-footnote-id]: Footnote '[^missing]' not found
--> document.qmd:1:31
undefined-reference-label
Emitted when a reference-style link points to a label that has no matching definition.
undefined-footnote-id
Emitted when a footnote reference points to an ID that has no matching footnote definition.
undefined-anchor
Detects inline links whose #fragment destination has no matching anchor in the document.
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
undefined-anchor - Description
-
Flags
[text](#fragment)links where#fragmentdoes not match any anchor that will exist in the rendered output. Anchor sources include explicit{#id}attributes on headings, fenced divs, code blocks, spans, and chunk labels, plus auto-generated heading IDs (when theauto_identifiersextension is enabled). Matching is case-sensitive, mirroring how browsers resolve URL fragments.Links with a path component (
other.qmd#frag), absolute URLs (https://example.com#frag), and bare back-to-top links (#) are not flagged. In bookdown projects, sibling chapters are scanned because bookdown’s gitbook renderer rewrites cross-chapter anchors. Quarto books render each chapter to a separate HTML page and are not scanned cross-chapter.When the
citationsextension is enabled, links of the form[text](#ref-citekey)are recognized as overriding a citation’s link text (Pandoc renders bibliography entries withid="ref-<citekey>"), so they resolve as long as@citekeyappears somewhere in the document.Anchors declared via raw HTML
<a id="x">/<a name="x">are not currently inspected, so links to them may be flagged as undefined.<div id="x">blocks are recognized.
Example violation:
# Real Heading {#real}
See [the typo](#reel).Diagnostic:
warning[undefined-anchor]: Anchor '#reel' not found in document
--> document.qmd:3:16
unused-definitions
Detects reference labels and footnote definitions that are declared but never referenced.
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
unused-definition-label,unused-footnote-id - Description
-
Flags unused reference definitions (
[label]: ...) and unused footnote definitions ([^id]: ...). This helps keep documents tidy and avoids dead references that can accumulate over time. When project metadata is available (for example in Quarto/Bookdown project lint runs), usage is resolved across project documents to reduce cross-file false positives.
Example violation:
Text with one note[^1].
[^1]: Used note.
[^2]: Unused note.
[used]: https://example.com
[unused]: https://unused.example.comDiagnostic:
warning[unused-footnote-id]: Footnote '[^2]' is never used
--> document.qmd:4:1
warning[unused-definition-label]: Reference definition '[unused]' is never used
--> document.qmd:7:1
unused-definition-label
Emitted when a reference definition ([label]: ...) is declared but never referenced anywhere in the document (or project, when project metadata is available).
unused-footnote-id
Emitted when a footnote definition ([^id]: ...) is declared but never referenced.
citation-keys
Validates citation keys against loaded bibliographies and detects conflicts in inline bibliography entries.
- Severity
- Error for bibliography load/parse failures, Warning for undefined keys and duplicates
- Auto-fix
- No
- Requirements
-
Requires
extensions.citations = truein configuration - Diagnostic codes
-
bibliography-load-error,bibliography-parse-error,missing-bibliography-key,duplicate-bibliography-key,duplicate-inline-reference-id - Description
-
Checks that all cited keys (
[@key]) exist in the configured bibliography files. Also validates inline bibliography entries for duplicates and conflicts.
Example violation (undefined key):
---
bibliography: refs.bib
---
See @smith2020 and @jones2021.If jones2021 doesn’t exist in refs.bib:
warning[missing-bibliography-key]: Citation key 'jones2021' not found in bibliography
--> document.qmd:5:17
Example violation (bibliography load error):
---
bibliography: nonexistent.bib
---error[bibliography-load-error]: Failed to load bibliography nonexistent.bib: File not found
--> document.qmd:1:1
When it runs: Only when document metadata includes bibliography configuration and the citation extension is enabled.
bibliography-load-error
Emitted when a configured bibliography file cannot be opened (missing, unreadable, etc.).
bibliography-parse-error
Emitted when a bibliography file is opened successfully but contains entries that fail to parse.
missing-bibliography-key
Emitted when a @cite reference does not match any key in the loaded bibliography.
duplicate-bibliography-key
Emitted when the same key appears more than once across loaded bibliography files.
duplicate-inline-reference-id
Emitted when an inline bibliography entry collides with another inline entry or with a key from a loaded bibliography file.
crossref-as-link-target
Detects link destinations that begin with @, which is almost always a typo for # (anchor) or a misplaced cross-reference / citation key.
- Severity
- Warning
- Auto-fix
- Yes
- Requirements
-
Requires
extensions.citations = truein configuration (default for the Pandoc, Quarto, and R Markdown flavors). - Diagnostic codes
-
crossref-as-link-target - Description
-
In Pandoc/Quarto,
@keyis reserved for citations and cross-references and must stand alone, not appear inside a link’s(...)destination. Writing[Figure 2](@fig-2)produces a link with the literal URL@fig-2; the author almost always meant[Figure 2](#fig-2).
Example violation:
See [Figure 2](@fig-2) for details.Diagnostic:
warning[crossref-as-link-target]: Link target starts with '@'; cross-references and citation keys must stand alone, not appear as a link destination
--> document.qmd:1:16
|
1 | See [Figure 2](@fig-2) for details.
| ^
Auto-fix output:
See [Figure 2](#fig-2) for details.When it runs: On every inline link ([text](dest)) and inline image () whose destination’s leading non-whitespace character is @. Bare cross-references (@fig-2) and citation forms ([@smith2020]) outside of link destinations are not flagged.
chunk-label-spaces
Detects executable chunk labels containing whitespace (for example {r several words} or label="several words").
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
chunk-label-spaces - Description
-
Labels with spaces are accepted by Quarto execution, but cross-references often fail to resolve reliably. Use a stable identifier such as
several-wordsorseveral_wordsinstead.
missing-chunk-labels
Detects executable chunks that do not define a label (either inline or hashpipe style).
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
missing-chunk-labels - Description
-
Labels facilitate debugging. Add a label with either
#| label: my-chunkor inlinelabel=my-chunk.
figure-crossref-captions
Detects figure cross-references that point to chunk labels without a figure caption option.
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
figure-crossref-captions - Description
-
Bookdown figure cross-references (
\@ref(fig:...)) require a captioned chunk to create a resolvable figure label at render time. When the target chunk has alabelbut nofig-cap/fig.cap, the crossref will not resolve.
unknown-emoji-alias
Detects :alias: emoji shortcodes that are not recognized.
- Severity
- Warning
- Auto-fix
- No
- Requirements
-
Requires
extensions.emoji = truein configuration - Diagnostic codes
-
unknown-emoji-alias - Description
- Checks parsed emoji aliases against the emoji shortcode dataset and warns when an alias is unknown.
Example violation:
Looks good :smile:, but this one is wrong :not-a-real-emoji:.Diagnostic:
warning[unknown-emoji-alias]: Unknown emoji alias ':not-a-real-emoji:'
--> document.qmd:1:40
html-entities
Detects malformed HTML named entity references in inline prose.
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
html-entities - Description
-
Pandoc and Quarto pass HTML named entities like
…through to the output unchanged, so a typo (&ellips;) or a missing trailing semicolon (&numeroinstead of№) silently produces wrong output. This rule flags three conservative cases:&NAME;whereNAMEis not in the HTML5 named-entity table.&NAME(no semicolon) where adding the semicolon would produce a known entity. This avoids firing on plain prose like “Tom & Jerry” or “AT&T”, since those words are not entity names.&NAME(no semicolon, length ≥ 4) whereNAMEis one edit away from a known entity (e.g.&hellp→…). Far-from-anything words are left alone to keep prose like “Procter &Gamble” quiet.
The rule deliberately ignores numeric character references (
{,ꯍ) for now and does not scan code spans, code blocks, raw HTML, inline/display math, link destinations, attributes, YAML metadata, or comments.
Example violations:
This is &ellips; wrong.
Section &numero 5 of the report.Diagnostics:
warning[html-entities]: Unknown HTML entity '&ellips;'
--> document.qmd:1:9
= help: did you mean '…'?
warning[html-entities]: HTML entity '&numero' is missing a trailing ';'
--> document.qmd:3:9
= help: write '№' to encode the character
link-text-is-url
Detects inline links whose visible text is identical to the destination URL (e.g. [https://example.com/](https://example.com/)) — typically an artifact of HTML→Markdown conversion — and offers to rewrite them as an autolink.
- Severity
- Warning
- Auto-fix
-
Yes (replaces the bracket form with
<url>) - Diagnostic codes
-
link-text-is-url - Description
-
The rule fires only when all of the following hold:
- the link is inline (
[text](url)), not reference-style; - the link text is plain — no nested emphasis, code, or other inline structure;
- the rendered link text is byte-identical to the destination URL, including any trailing slash;
- the link has no title;
- the URL passes the dialect’s autolink validator (CommonMark §6.4 schemes / email shape; the Pandoc dialect is laxer).
The byte-exact text-vs-URL check is intentional: changing
[A](B)to<A>rewrites where the link points. The rule never silently changes a destination, so cases like[https://example.net/](https://example.net)(text and URL differ by a trailing slash) are skipped even though they look “almost” duplicated. Use the LSP code action to convert those manually if intended. - the link is inline (
Example violation:
See [https://example.com/](https://example.com/) for details.Diagnostic:
warning[link-text-is-url]: Link text is identical to the URL; an autolink is shorter and clearer.
--> document.md:1:5
= help: rewrite as `<https://example.com/>`
Auto-fix output:
See <https://example.com/> for details.adjacent-footnote-refs
Detects footnote references placed back-to-back ([^a][^b]) where the rendered superscripts run together (e.g. footnotes 7 and 8 look like footnote 78).
- Severity
- Warning
- Auto-fix
- Yes (inserts a space between the references)
- Requirements
-
Requires
extensions.footnotes = truein configuration - Diagnostic codes
-
adjacent-footnote-refs - Description
- When two footnote references appear with no intervening character, most renderers emit the superscript markers as a single visually-merged run. Inserting a single space between them keeps the markers distinct without changing the prose.
Example violation:
See the prior reports[^a][^b] for context.Auto-fix output:
See the prior reports[^a] [^b] for context.footnote-ref-in-footnote-def
Detects footnote references ([^id]) that appear inside a reference-style footnote definition body, where pandoc silently parses them as literal text instead of resolving the reference.
- Severity
- Warning
- Auto-fix
- No (the user must decide whether to inline the prose, restructure to lift the reference out of the definition body, or drop it)
- Requirements
-
Requires
extensions.footnotes = truein configuration (default for Pandoc, Quarto, R Markdown, and GFM flavors). - Diagnostic codes
-
footnote-ref-in-footnote-def - Description
-
Pandoc footnotes do not nest. Inside a
[^x]: ...definition body, any[^id]reference is silently parsed as a literalStr— the would-be link disappears from the output with no warning. The same applies to references nested arbitrarily deep inside that body (inside emphasis, strong, strikeout, links, blockquotes, lists, or inline footnotes).This rule surfaces the silent drop at lint time so the user notices before the document is rendered. After the parser fix that aligns panache with pandoc on this case, the inner references no longer appear as
FOOTNOTE_REFERENCEnodes; the rule scans the definition body’sTEXTtokens directly for[^id]byte patterns, which naturally skips code spans, math, raw HTML, and other CST-distinct constructs.References at the top level (outside any definition body) and inside a top-level inline footnote
^[...]are not flagged — pandoc resolves those normally.
Example violation:
Outer[^a].
[^a]: Body has [^b] ref and **bold [^c] inside** wrapper.
[^b]: B body.
[^c]: C body.Diagnostic:
warning[footnote-ref-in-footnote-def]: Footnote reference '[^b]' inside a footnote definition body
is silently dropped by pandoc (rendered as literal text)
--> document.qmd:3:16
= help: footnotes do not nest in pandoc; inline the prose, restructure to
keep the reference outside the definition body, or remove it
stray-fenced-div-markers
Detects runs of three or more colons (:::, ::::, …) that appear inside inline text (paragraphs, tight list items, definition list bodies, table cells) instead of parsing as fenced div markers.
- Severity
- Warning
- Auto-fix
- No
- Requirements
-
Requires
extensions.fenced-divs = truein configuration (default for Pandoc, Quarto, and R Markdown flavors). - Diagnostic codes
-
stray-fenced-div-markers - Description
-
Pandoc fenced divs use
:::(or longer colon runs) for both openers (with an attribute or class) and closers (bare colons). Pandoc only treats:::as a marker when it starts a line on its own; if the colons end up embedded in paragraph text—a stray closer with no matching opener, a closer accidentally glued to the previous line, or a marker with extra words after it—they silently render as:::characters in the prose and the div either never opens or never closes.Quarto’s runtime emits a warning when it sees stray
:::, but exits 0, which makes the issue invisible to CI/Makefile workflows. This rule fills that gap by flagging the same condition at lint time.The rule fires on any run of three or more
:characters that survives as plain text inside paragraph-like inline content (paragraphs, tight list items, definition list bodies, table cells). Code spans (`:::`), indented code blocks, and raw HTML blocks are not flagged because the colons there are not inline text. Authentic prose mentions of:::(writing about Pandoc syntax) should be wrapped in backticks anyway—runs of three or more consecutive colons are otherwise extremely rare in natural text.
Example violations:
::: warning
The fence count on the opener and closer don't match.
::::::: {lang=en-US}
[contact Ms. Nebbercracker]{lang=en-US}:::[]{#hmm}
::: {lang=zh-TW}
bla
:::Diagnostic:
warning[stray-fenced-div-markers]: '::::' appears as text, not as a fenced div marker
--> document.qmd:3:1
= help: Pandoc only treats ':::' as a fenced div marker when it starts a line
on its own (optionally followed by a class or attributes). Add a
newline before it, or wrap it in backticks if it's intentional text
When a ::: run sits at the start of a line and the rest of the line forms a valid fence shape (opener or closer), but a preceding non-blank line pulls it into a paragraph, the diagnostic is sharpened to name the actual failure mode:
warning[stray-fenced-div-markers]: ':::' looks like a fenced div marker, but
the preceding line pulls it into a paragraph
--> document.qmd:2:1
= help: Insert a blank line above this line so Pandoc parses it as a fenced
div instead of paragraph text
math-syntax
Detects structural problems in the TeX content of inline ($...$) and display ($$...$$, \[...\], \begin{env}...\end{env}) math: unbalanced braces and unclosed or mismatched environments.
- Severity
- Error
- Auto-fix
- No
- Requirements
-
Requires a
tex-math-*extension (e.g.extensions.tex-math-dollars, default for Pandoc, Quarto, and R Markdown flavors). With no math extension enabled there are no math spans, so the rule never fires. - Diagnostic codes
-
math-unclosed-group,math-unexpected-close-brace,math-unclosed-environment,math-mismatched-environment,math-unexpected-end - Description
-
The math parser captures math content losslessly even when it is malformed, so a stray brace or unterminated environment never breaks parsing or formatting — but it is build-breaking downstream:
quarto renderto PDF hard-fails on an unclosed brace or mismatched environment, and MathJax/KaTeX silently drop the equation. That is why these ride aterrorseverity. This rule surfaces the structural problems the parser already detected, with the diagnostic pointing at the offending byte (the unclosed{, the stray}, or the mismatched\end).The check is purely syntactic: TeX is a macro language, so the rule does not validate command names, argument arity, or semantics — only brace and environment nesting. Because a macro can expand to braces or an environment the structural parser cannot see, valid TeX can occasionally look unbalanced; in that rare case disable the rule with
[lint.rules] math-syntax = falseor an ignore directive.
Example violation:
The mass-energy relation is $E = mc^{2$.Diagnostic:
error[math-unclosed-group]: unclosed `{` group
--> document.qmd:1:38
|
1 | The mass-energy relation is $E = mc^{2$.
| ^
Resolution: Balance the braces or close the environment. No auto-fix is provided because the correct repair (which brace to add, where) is an author-intent decision.
math-unclosed-group
Emitted when a { is never closed before the end of the math content.
math-unexpected-close-brace
Emitted when a } appears with no matching {.
math-unclosed-environment
Emitted when a \begin{env} is never closed by a matching \end{env}.
math-mismatched-environment
Emitted when a \begin{a} is closed by \end{b} with a different name.
math-unexpected-end
Emitted when an \end appears with no open \begin.
YAML diagnostics
Panache emits YAML diagnostics when embedded YAML content is invalid. These apply to both document frontmatter (--- ... ---) and executable chunk hashpipe options (#| ...).
yaml-parse-error
- Severity
- Warning
- Auto-fix
- No
- Description
- The YAML lexer/parser could not interpret the content (malformed flow sequences, unterminated strings, etc.).
Example (hashpipe):
```{r}
#| echo: [
1 + 1
```Diagnostic:
warning[yaml-parse-error]: YAML parse error: ...
--> document.qmd:2:10
yaml-structure-error
- Severity
- Warning
- Auto-fix
- No
- Description
- The YAML parsed successfully but its top-level shape is not valid for the context (for example, frontmatter that is not a mapping, or a hashpipe block that does not produce a mapping of options).