Lint Rules
Reference catalog 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)
Fix safety
Auto-fixes carry a safety level that controls when panache lint --fix applies them:
safe-
The fix preserves the document’s meaning and only tidies syntax. Safe fixes are applied by a plain
--fix. unsafe-
The fix may change the document’s meaning (for example, deleting a key). Unsafe fixes are skipped by a plain
--fix; pass--unsafe-fixesalongside--fixto apply them too. When a run leaves unsafe fixes unapplied, the CLI prints how many are available.
In the editor (LSP), unsafe fixes are still offered as individual quick fixes (labeled (unsafe)), but they are excluded from the aggregate “Fix all auto-fixable lint issues” action, mirroring the safe-by-default CLI behavior.
The “Auto-fix” field of each rule below notes when its fix is unsafe.
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.
empty-values
Flags YAML block-mapping keys whose value is an implicit null: a key with nothing after the colon.
- Severity
- Warning
- Auto-fix
-
Yes (unsafe). Deletes the empty key’s whole line. Marked unsafe because dropping a key changes the document’s data, so it is applied only with
--unsafe-fixes. - Diagnostic codes
-
empty-values - Description
-
Modeled on yamllint’s
empty-values. A key with no value (title:followed by a newline) parses as YAML null. That is occasionally intentional, but far more often a value the author forgot to fill in. The rule walks document frontmatter and hashpipe cell options alike (both embed the same YAML CST).An explicit null is a deliberate value and is never flagged, so write
title: null(ortitle: ~) when the empty value is on purpose. The rule covers block mappings only; flow mappings ({a: }) and block sequences are not flagged.
Example violation:
---
title:
author: Jane Roe
---Diagnostic:
warning[empty-values]: Key `title` has an empty value (implicit null)
--> document.md:2:1
|
2 | title:
| ^^^^^
Resolution: Supply a value, delete the key, or write an explicit null if the empty value is intentional. The auto-fix takes the “delete the key” path, but because that discards data it is unsafe and runs only under --unsafe-fixes.
consumer-divergence
Flags a plain YAML scalar whose resolved type or value differs across the document’s active YAML consumers—a real cross-toolchain ambiguity, not a style opinion.
- Severity
- Warning
- Auto-fix
-
Yes (unsafe). Single-quotes the value, forcing a string under every consumer. Marked unsafe because if the author meant the boolean or integer, quoting changes the value for the YAML 1.1 stage, so it is applied only with
--unsafe-fixes. - Diagnostic codes
-
consumer-divergence - Requirements
-
Quarto flavor (
flavor = "quarto"). Only Quarto frontmatter is read under two different YAML versions; every other context has a single consumer and cannot diverge. - Description
-
A Quarto document’s frontmatter is parsed twice: by Quarto’s js-yaml (YAML 1.2 core) and by pandoc’s libyaml (≈ YAML 1.1). The two versions resolve some plain scalars to different values. This rule flags only those genuine divergences. Unlike yamllint’s
truthy/octal-values, it stays silent when the value is unambiguous for the document’s consumers.Detected divergences:
- Booleans (the “Norway problem”). YAML 1.1 reads
y,n,yes,no,on,off(and case variants) as booleans; YAML 1.2 core reads onlytrue/false. Socountry: nois the booleanfalseto pandoc but the string"no"to Quarto. - Leading-zero integers. YAML 1.1 reads
0755/010as octal; js-yaml reads them as strings (it accepts only0o…octal). So the two stages disagree onmode: 0755(integer493vs string).
Quoted, literal, and folded scalars are never flagged (their string tag is pinned), and neither are unambiguous values such as
true,42,3.14,.inf, or plain words. The rule covers block mappings only; flow mappings and sequence items are not yet inspected. - Booleans (the “Norway problem”). YAML 1.1 reads
Example violation:
---
country: no
mode: 0755
---Diagnostic:
warning[consumer-divergence]: Key `country`: value `no` is the boolean `false` to pandoc (YAML 1.1) but the string "no" to Quarto's js-yaml (1.2)
--> document.qmd:2:10
|
2 | country: no
| ^^
Resolution: Quote the value to force a string under every consumer (country: 'no'), or write the explicit true/false (or canonical integer) you mean. The auto-fix takes the quoting path, but because that changes the value for the YAML 1.1 stage it is unsafe and runs only under --unsafe-fixes.
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, footnote, and cross-reference label definitions.
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
duplicate-reference-labels - Description
-
Each reference label, footnote ID, and cross-reference label must be unique within a document. Duplicate definitions cause ambiguity. Only the first definition is used, making the others ineffective. Cross-reference detection follows the active flavor’s extensions, so flavor-specific declaration syntax (for example bookdown
(\#eq:label)under RMarkdown) is included. -
Reference-link and footnote labels collide across files only when those files are merged into a single Pandoc pass. That happens for Quarto includes (
includeshortcodes, which splice the child into its parent) and for bookdown projects (_bookdown.yml), where chapters are concatenated. In those cases a footnote or reference label repeated across files is reported. Quarto renders each.qmdas its own document, so the same label in two independently-rendered files is not a conflict and is not flagged. Cross-reference labels (such as figure and section IDs) resolve across documents and are checked project-wide regardless.
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 and 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 or cross-reference points to a label that has no matching definition. Cross-references using an extension prefix listed in crossref-prefixes are not validated, since their targets are defined by a mechanism Panache does not model.
undefined-footnote-id
Emitted when a footnote reference points to an ID that has no matching footnote definition.
On flavors with inline footnotes, a label containing whitespace ([^and a note about them]) is left to reversed-footnote-marker instead: pandoc never reads such a bracket as a footnote reference in the first place, so reporting a missing definition would be misleading.
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">or<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.
duplicate-yaml-anchor
Detects a YAML anchor name declared more than once within a single embedded YAML document (frontmatter or a hashpipe #| cell-options block).
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
duplicate-yaml-anchor - Description
-
A repeated
&nameanchor is valid YAML 1.2 — the last definition wins — but every real consumer (pandoc/libyaml, js-yaml, Ryaml) accepts it, so it is never a parse error. A duplicate is almost always an accident, so the later declaration is flagged. Anchor scope is per document, so the same name reused after a---document boundary is not a duplicate. Mirrors yamllint’sanchors: forbid-duplicated-anchors. No auto-fix is offered: whether to rename or drop the duplicate depends on intent.
Example violation:
---
base: &defaults
timeout: 30
prod: &defaults
timeout: 60
---Diagnostic:
warning[duplicate-yaml-anchor]: YAML anchor `&defaults` is defined more than once
--> document.md:4:1
unused-yaml-anchor
Detects a YAML anchor that is declared but never referenced by an alias within the same embedded YAML document (frontmatter or a hashpipe #| cell-options block).
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
unused-yaml-anchor - Description
-
An anchor (
&name) with no matching*namealias in the same document is valid YAML but dead weight. Usage is resolved per document (aliases in a later document across a---boundary do not count, matching YAML’s per-document anchor scope). Mirrors yamllint’sanchors: forbid-unused-anchors. No auto-fix is offered: removing the anchor may not be the intended resolution.
Example violation:
---
theme: &brand
primary: blue
---Diagnostic:
warning[unused-yaml-anchor]: YAML anchor `&brand` is never used
--> document.md:2:8
unsupported-metadata-key
Detects a frontmatter mapping key that pandoc’s metadata layer refuses to convert, which aborts the whole document with an error pointing at the top of the metadata block rather than at the key.
- Severity
- Error
- Auto-fix
- No (quoting the key invents a key the author never wrote, and the usual real-world trigger wants the line moved out of the frontmatter instead)
- Requirements
- Requires a flavor whose frontmatter reaches pandoc as metadata — Pandoc, Quarto, or R Markdown. GFM, CommonMark, MultiMarkdown, mdsvex, and MyST frontmatter is not read by pandoc, so the rule is not registered there.
- Diagnostic codes
-
unsupported-metadata-key - Description
-
Pandoc parses frontmatter with libyaml and then converts the result to metadata, where every mapping key must be a string. libyaml accepts a collection key (
[a, b]: v,{x: 1}: v, an explicit? - a⏎- b⏎: v) or an alias key (*anchor: v), so no YAML parse error fires — pandoc fails a step later withError parsing YAML metadata at (line 1, column 1): Error in $: Non-string keys are not supportedand nothing renders. The reported position is the start of the metadata block, so on a long header it says nothing about which key is at fault. This rule points at the key.
Collection keys are rejected at any depth, including inside a top-level sequence, and an alias key is rejected even when its anchor holds a plain scalar (pandoc reports
Non-string key aliasthere), so every alias key is flagged without resolving the anchor.Non-string scalar keys are fine and never flagged: pandoc stringifies them, so
1: one,no: nope, and2024-01-01: launchall convert. This is a key-shape rule, not a YAML 1.1 typing rule — for values whose type differs between pandoc and Quarto’s js-yaml, seeconsumer-divergence.Only frontmatter is checked. Hashpipe
#|cell options are read by js-yaml (Quarto) or the Ryamlpackage (knitr), neither of which restricts key shape, and they never reach pandoc’s metadata layer.A frontmatter block whose top level is a scalar or a sequence is not this error: pandoc declines to treat such a block as metadata at all and re-reads it as document content. Panache parses those the same way, so there is nothing to report.
Example violation:
---
title: Notes
[1]: https://example.com
---Diagnostic:
error[unsupported-metadata-key]: sequence used as a metadata key is not
supported by pandoc
--> document.qmd:3:1
= note: pandoc converts frontmatter to metadata, where every key must be a
string; it fails the whole document with `Non-string keys are not
supported`
= help: quote the key to make it a string, or move the line out of the
frontmatter if it was not meant to be metadata
Correct forms:
The line was meant to be a link reference definition, so it belongs in the document body:
---
title: Notes
---
[1]: https://example.comOr, if it really is metadata, quote the key so it is a string:
---
title: Notes
"[1]": https://example.com
---citation-keys
Validates citation keys against loaded bibliographies and detects conflicts in inline bibliography entries.
- Severity
- Error for bibliography load or 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:20
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. Built-in Quarto cross-references (@fig-, @tbl-, …) are exempt; for extension-injected prefixes such as pseudocode’s @algo-, list them under crossref-prefixes so they are treated as cross-references rather than citations.
duplicate-bibliography-key
Emitted when the same key appears more than once across loaded bibliography files. The diagnostic anchors to a bibliography declaration in the document’s own YAML frontmatter. A duplicate confined to a project-level bibliography (declared in _quarto.yml/_metadata.yml rather than the document) is a defect of that shared file, not of any one document: instead of being repeated for every inheriting document, it is reported once, anchored to the manifest’s own bibliography: value. This manifest check runs when a project directory is linted or when the manifest is targeted explicitly; a single-document lint stays quiet about the ambient manifest.
duplicate-inline-reference-id
Emitted when an inline bibliography entry collides with another inline entry or with a key from a loaded bibliography file.
citation-nonbreaking-space
Detects a breakable space (or a source line break) between text and a bracketed citation ([@key]), where line wrapping can strand the rendered citation — a lone [1] or (Smith 2020) — at the start of a line.
- Severity
- Warning
- Auto-fix
- Yes (replaces the space with a non-breaking one)
- Requirements
-
Requires
extensions.citations = truein configuration (default for the Pandoc, Quarto, and R Markdown flavors). - Diagnostic codes
-
citation-nonbreaking-space - Description
-
Pandoc renders the space before
[@key]as an ordinary breakable space in every output format, so whether the citation lands at the start of a line is left to the final typesetting — and the author usually cannot know at writing time whether the document will render with a numeric or an author-year citation style. Tying the citation to the preceding word with a non-breaking space is safe under both. The rule flags a plain space, tab, or source line break directly before a bracketed citation (including suppressed-author citations like[-@key]). A citation that starts a paragraph, follows a hard line break, or is already tied with\or a literal U+00A0 is not flagged. In-text citations (@key) are out of scope.The auto-fix rewrites the gap to
\(Pandoc’s non-breaking space escape). Whenall-symbols-escapableis disabled,\would not parse as an escape, so the fix inserts a literal U+00A0 character instead.
Example violation:
Some important fact [@smith2020].Auto-fix output:
Some important fact\ [@smith2020].unspaced-citation
Detects an in-text citation (@key) glued to the preceding word, where Pandoc leaves it as literal text rather than a citation — but only when key names a reference the document actually knows about.
- Severity
- Warning
- Auto-fix
- No
- Requirements
-
Requires
extensions.citations = truein configuration (default for the Pandoc, Quarto, and R Markdown flavors). - Diagnostic codes
-
unspaced-citation - Description
-
Pandoc’s
notAfterStringrule means an author-in-text@keyis only recognized when its@does not directly follow a word character. Soword@smith2004(and, for the same reason, an email like[email protected]) is parsed as ordinary text, and the intended citation silently fails to render. The rule flags such a glued@keyso the mistake is caught at writing time.To avoid false positives on email addresses, handles, and other incidental
@text, the rule fires only when the trailing key is a defined citation key: an entry in a loaded bibliography or an inline YAMLreferences:entry. A key the document does not define is left alone.There is no auto-fix because two distinct repairs are valid and the rule cannot know which the author intends: insert a space (
word @smith2004) or wrap the key in brackets (word[@smith2004]). For a literal@that should never be a citation, escape it as\@.
Example violation:
The result by work@smith2004 built on earlier findings in @smith2004.crossref-as-link-target
Detects link destinations that begin with @, which is almost always a typo for # (anchor) or a misplaced cross-reference or 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
- Requirements
-
extensions.fenced-code-attributes = true(the default for Quarto and R Markdown flavors). - 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
- Requirements
- A flavor with executable chunks (Quarto or R Markdown).
- Diagnostic codes
-
missing-chunk-labels - Description
-
Labels facilitate debugging. Add a label with either a hashpipe option or inline
label=my-chunk. The suggested hashpipe comment prefix matches the chunk language (#|for R or Python,//|for C++ or Rust,--|for SQL, and so on).
figure-crossref-captions
Detects figure cross-references that point to chunk labels without a figure caption option.
- Severity
- Warning
- Auto-fix
- No
- Requirements
- A flavor with executable chunks (Quarto or R Markdown).
- 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, comments, or the verbatim bodies of MyST code and math directives ({code-cell},{code-block},{code},{math}).
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/)), which is typically an artifact of HTML to 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.Bare URIs (under the
autolink-bare-urisextension, e.g. a plainhttps://example.comin GFM) are never flagged: they carry no brackets, are already the shortest form, and parse as autolinks rather than[text](url)links. - 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.blank-line-in-inline-footnote
Detects an apparent inline footnote whose opening ^[ and closing ] are separated by a blank line.
- Severity
- Warning
- Auto-fix
- No (removing the blank line would merge paragraphs and change document structure based on inferred intent)
- Requirements
-
Requires
extensions.inline-footnotes = truein configuration - Diagnostic codes
-
blank-line-in-inline-footnote - Description
-
Pandoc inline footnotes may wrap across ordinary line breaks, but they cannot contain multiple paragraphs. A blank line ends the paragraph before pandoc finds the closing bracket, so the entire apparent footnote—including its
^[and]markers—is rendered as literal text.The rule reports an unescaped
^[at the end of a paragraph only when the paragraph after the blank line contains an apparent closing]. This avoids warning about a lone^[used as literal notation.
Example violation:
This has an inline note.^[The note starts here.
It appears to continue here.]blank-line-in-display-math
Detects standalone $$ display-math delimiters separated by a blank line.
- Severity
- Warning
- Auto-fix
- No (removing the blank line would merge paragraphs and change document structure based on inferred intent)
- Requirements
-
Requires
extensions.tex-math-dollars = true—atex-math-*extension enabled by default for Pandoc, Quarto, and R Markdown flavors. - Diagnostic codes
-
blank-line-in-display-math - Description
-
Pandoc display math may span ordinary line breaks, but it cannot span a blank line. The blank line closes the opening paragraph before Pandoc sees the second delimiter, so both
$$markers are rendered as literal text in HTML and PDF output rather than as an equation.The rule reports the opening delimiter only when the immediately following paragraph contains another standalone
$$line. It therefore leaves lone literal dollar markers alone.
Example violation:
$$
a
$$Resolution: Remove the blank line to create a display-math block, or escape the dollar signs when they are intended as literal text. No auto-fix is offered because either interpretation may be correct.
footnote-swallowed-by-bracket
Detects inline footnotes (^[note]) followed immediately by [ or (, where pandoc consumes the note body as a link label and the footnote silently disappears.
- Severity
- Warning
- Auto-fix
-
Yes (inserts a space after the footnote’s closing
]; safe for the[form, unsafe for the(form) - Requirements
-
Requires
extensions.inline-footnotes = truein configuration - Diagnostic codes
-
footnote-swallowed-by-bracket - Description
-
Pandoc only reads
^[as an inline-footnote opener when the closing]is not followed by another bracket run. When a[follows, the two bracket groups are parsed as a reference link and the whole span degrades to literal text — the^[markers show up verbatim in the output. When a(follows, it is worse:[note](dest)becomes an ordinary link and the^is left behind as a stray caret, so the document renders cleanly and the lost footnote is easy to miss.Inserting a single space after the footnote’s closing
]restores the footnote and leaves the following link intact. For the(form the fix is marked unsafe, because a stray^in front of an intended link is an equally plausible reading; apply it with--unsafe-fixesonce you have confirmed a footnote was meant.
Example violation:
Coordinates^[Notes on coordinates.][Figure 1](#fig-1) follow.Auto-fix output:
Coordinates^[Notes on coordinates.] [Figure 1](#fig-1) follow.reversed-footnote-marker
Detects [^...] brackets that pandoc will not read as a footnote reference, which usually means the inline-footnote marker was written backwards — [^note] where ^[note] was meant.
- Severity
- Warning
- Auto-fix
-
Yes, but unsafe (swaps the
[^opener for^[; the brackets currently render as something, so promoting them to a footnote changes the output). Withheld when a[or(follows the closing], where the swap would not produce a footnote at all. - Requirements
-
Requires
extensions.inline-footnotes = truein configuration - Diagnostic codes
-
reversed-footnote-marker - Description
-
Pandoc’s reference form takes a bare label:
[^has to be followed by label characters with no whitespace and a closing]on the same line. Prose between the brackets breaks all of that, and the bracket run quietly degrades into something else — literal text, or a citation when the prose happens to contain an@key. Because nothing disappears from the rendered output, the missing note is easy to overlook.A well-formed but undefined label (
[^missing]) is a different problem and stays withundefined-footnote-id. Links whose text merely starts with a caret ([^text](dest)) are left alone: they render as working links, so a stray caret is the likelier reading.The fix is marked unsafe because it changes what the document renders: the brackets stop being prose and become a footnote. Apply it with
--unsafe-fixesonce you have confirmed a note was meant.No fix is offered when a
[or(follows the closing]. There the swap would leave the note just as absent while creating afootnote-swallowed-by-bracketdefect —^[note](dest)is a stray caret plus a link, and^[note][ref]is literal text. Deciding where the note ends is the author’s call.
Example violation:
Coordinates are read-only [^and they are derived from the street number].Auto-fix output:
Coordinates are read-only ^[and they are derived from the street number].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 literalStrand 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
footnote-after-image
Detects a footnote hanging off a standalone image, which keeps the image from being promoted to a figure and silently discards its caption.
- Severity
- Warning
- Auto-fix
- No (the user must decide whether the note belongs inside the caption or beside the figure as a separate paragraph)
- Requirements
-
Requires
extensions.footnotes = truein configuration (default for Pandoc, Quarto, R Markdown, and GFM flavors) andextensions.implicit-figures = true(default for Pandoc, Quarto, and R Markdown). - Diagnostic codes
-
footnote-after-image - Description
-
Pandoc’s
implicit_figurespromotes an image to a figure only when the image is alone in its paragraph. A trailing footnote breaks that condition, so{#fig-1} ^[A note about the figure.]parses as
Para [Image, SoftBreak, Note]rather thanFigure. Nothing errors: the caption text survives only as the image’saltattribute and never renders as a caption. Under Quarto the consequence is louder, because the#fig-id no longer labels a figure and@fig-1renders as an unresolved**?@fig-1**.Both placements are flagged: the footnote on the line below (which pandoc reads as lazy continuation of the image’s paragraph) and the footnote on the same line. Reference-style footnotes (
[^1]) demote the figure exactly as inline ones (^[...]) do, and are flagged alike.The rule stays quiet when the demotion costs nothing. An image with neither caption text nor an id (
) loses only an empty figure wrapper. A paragraph holding prose alongside the footnote is also skipped, because removing the footnote would not restore the figure there and the advice would be wrong.There is deliberately no auto-fix. The two resolutions mean different things: moving the footnote inside the caption makes it part of the caption, while inserting a blank line restores the figure and leaves the note as a separate paragraph. Splicing a multi-line footnote body into a caption is also not a mechanical edit.
Example violation:
{#fig-1}
^[A note about the figure.]Diagnostic:
warning[footnote-after-image]: footnote attached to a standalone image keeps it
from becoming a figure
--> document.qmd:2:1
= note: an image becomes a figure only when it is alone in its paragraph; the
trailing footnote demotes it
= note: the caption text will render as the image's alt attribute, not as a
caption
= note: the id no longer labels a figure, so cross-references to it will not
resolve
= help: move the footnote inside the caption, as in
`![Caption. ^[note]](img.jpg)`, or separate it from the image with a
blank line to keep the figure
Correct forms:
![A caption ^[A note about the figure.] here.](img.jpg){#fig-1}{#fig-1}
^[A separate remark.]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
swallowed-list-marker
Detects a line that looks like a bullet or numbered list marker but was absorbed into the paragraph above, because no blank line separates them.
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
swallowed-list-marker - Description
-
Pandoc-markdown never lets a list interrupt a paragraph. A marker on the line directly below prose is therefore not a list at all—it is lazy continuation text, and reformatting splices the markers into the sentence above:
Reviewing this incident, the conclusion may only be - train the registration staff - scan the coordinate columnbecomes one paragraph reading
Reviewing this incident, the conclusion may only be - train the .... Pandoc produces exactly the same singlePara, so this is an authoring trap rather than a formatting bug—but a silent one, which is what this rule fixes.Under the CommonMark and GFM flavors bullets and ordered lists starting at
1do interrupt a paragraph, so they parse as real lists and are never flagged. An ordered list starting at any other number still cannot interrupt, so2.after a prose line is flagged there too, with a help note naming the CommonMark rule instead.A run of consecutive marker lines produces a single diagnostic anchored at the first one, because the remedy is a single blank line above the run. Inserting one before every item would instead produce a loose list, which renders each item wrapped in its own paragraph.
The rule is deliberately narrower than Pandoc’s own marker syntax, to keep it quiet on ordinary prose. It recognizes
-,*, and+bullets, and decimal markers of at most two digits followed by.or). It does not flag alphabetical, roman, or example-list markers (a.,iv.,(@ok)), all of which occur often in normal sentences, nor longer numbers, so2024. was a good yearis left alone. Markers indented four or more columns, thematic-break shapes (- - -), and escaped markers (\-) are also skipped.
Example violation:
Reviewing this incident, the conclusion may only be
- train the registration staff
- scan the coordinate columnDiagnostic:
warning[swallowed-list-marker]: '-' looks like a list marker, but the preceding
line pulls it into a paragraph
--> document.qmd:2:1
= note: 2 consecutive lines here start with a list marker; all of them are
reflowed into the paragraph above
= help: Pandoc-markdown never lets a list interrupt a paragraph. Insert a
blank line above this line to start a real list, or escape the marker
('\-') if it is meant as prose
Resolution: there is no auto-fix because two different intentions are plausible and the rule cannot tell them apart. If a list was meant, insert a blank line above the run; if the marker is prose (Prices range from $5 / - $10 depending on volume), escape it. A blank line is also not reliably sufficient on its own: any prose following the run would become lazy continuation of the final list item, and would need its own blank line too.
unspaced-list-marker
Detects a line consisting of -\ in paragraph text inside a list.
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
unspaced-list-marker - Description
-
A list marker needs whitespace after the hyphen. A line containing only
-\, possibly indented, can look like an empty nested item, but the hyphen remains paragraph text. In Pandoc, the backslash before the newline adds a hard line break without creating another list item.This rule is enabled by default in all flavors, including when the
escaped-line-breaksextension is disabled. It flags the missing marker space whether the backslash becomes a hard break or stays literal text. The diagnostic covers only the hyphen and backslash.Detection is limited to standalone
-\lines in list paragraphs. Ordinary soft line breaks, escaped hyphens (\-), code, and text inside inline spans are left alone. This is separate fromswallowed-list-marker, which detects valid marker shapes absorbed into preceding prose because of a missing blank line.
Example violation:
- **Case background:**
-\
- **Environmental concerns:**Diagnostic:
warning: [unspaced-list-marker] Missing whitespace after '-': this line is paragraph text
--> document.md:2:3
= help: If you intended a list item, add a space after '-' and check its
indentation. Escape the hyphen as '\-' if it is literal text
Resolution: add whitespace after the hyphen and check the indentation if a list item was intended. For a literal hyphen, escape it as \-. No automatic fix is offered because these choices express different intentions.
table-column-count
Detects a pipe table row carrying more cells than its delimiter row declares. Those cells are dropped when the table is rendered.
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
table-column-count - Description
-
A pipe table’s delimiter row owns its column count. Pandoc reads the count off that row and then pads or truncates every other row to it, so a header or data cell past the last column never reaches the output at all:
| a | b | c | |---|---| | 1 | 2 | 3 |renders as a two-column table holding only
a,b,1, and2. Thecand3cells are silently discarded, which usually means the delimiter row was mistyped rather than that the cells were unwanted.The opposite mismatch is harmless and is not flagged: a row shorter than the delimiter row is padded with empty cells, so no content is lost.
Under the
gfmandcommonmarkflavors the mismatch is not a table at all — those dialects require the header to match the delimiter row cell-for-cell, and the whole run stays a paragraph — so this rule has nothing to flag there.
Example violation:
| a | b | c |
|---|---|
| 1 | 2 | 3 |Diagnostic:
warning[table-column-count]: This row has 3 cells but the delimiter row declares
2 columns; the 1 extra cell is dropped when the table is rendered
--> document.qmd:1:9
Resolution: there is no auto-fix because two different intentions are plausible and the rule cannot tell them apart. If the extra columns were meant, widen the delimiter row (|---|---|---|); if they were not, delete the surplus cells. The formatter leaves such a table byte-for-byte as written for the same reason: normalizing it would either discard the author’s text or widen the delimiter row and change what pandoc renders.
inline-math-line-break
Detects a top-level TeX row break (\\) in inline math.
- Severity
- Warning
- Auto-fix
- No (removing one backslash or moving the expression into a multiline construct requires knowing the author’s intent)
- Requirements
-
Requires a
tex-math-*extension (e.g.extensions.tex-math-dollars, default for Pandoc, Quarto, and R Markdown flavors). - Diagnostic codes
-
inline-math-line-break - Description
-
At the top level of inline math,
\\is a row-break control symbol. It does not escape the following backslash:\\mathrmis parsed as a row break followed by the ordinary lettersmathrm, not as the command\mathrm. Some TeX engines accept a row break there, while other Markdown math renderers reject it, so the construct is not portable and is usually a doubled-backslash typo.The rule does not report row breaks nested inside multiline constructs such as
matrix,array, or\substack, and it does not report display math.
Example violation:
The differential is $\int f(x) \\mathrm{d}x$.Resolution: If a command was intended, remove the extra backslash (\mathrm). If the break was intentional, use a display-math environment appropriate for multiple rows. No auto-fix is offered because those changes have different meanings.
math-syntax
Detects structural problems in the TeX content of inline ($...$) and display ($$...$$, \[...\], \begin{env}...\end{env}) math: unbalanced braces, unclosed or mismatched environments, and unescaped dollar tokens.
- 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,math-unclosed-delimiter,math-unexpected-right,math-unexpected-dollar - 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}, the mismatched\end, or an unescaped$).The check is purely syntactic: TeX is a macro language, so the rule does not validate command names, argument arity, or semantics—only brace, environment, dollar, and
\left/\rightdelimiter 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.
math-unclosed-delimiter
Emitted when a \left delimiter is never closed by a matching \right (for example \left( x with no \right)).
math-unexpected-right
Emitted when a \right appears with no open \left.
math-unexpected-dollar
Emitted when an unescaped $ inside TeX math ends math mode prematurely.
quarto-schema
Validates document YAML frontmatter, code-cell options, and project config files (_quarto.yml, _metadata.yml) against Quarto’s machine-readable schema, flagging type mismatches and invalid enum values. (Unknown or misspelled keys are handled by the separate, opt-in quarto-schema-unknown-key rule.)
- Severity
- Warning
- Auto-fix
- No
- Diagnostic codes
-
quarto-schema-type-mismatch,quarto-schema-invalid-enum - Requirements
-
Quarto flavor (
flavor = "quarto"). Pandoc has no metadata schema, so the rule does not run for other flavors. - Description
-
Unlike pandoc—where metadata is an arbitrary mapping—Quarto ships a schema and validates against it. This rule reproduces the high-value parts of that check using a distilled copy of Quarto’s schema (vendored per quarto-cli release; see
assets/quarto-schema/.panache-source). It covers document frontmatter (--- ... ---), code-cell options (#| ..., validated against the cell’s engine schema—knitr for R cells, jupyter otherwise), and project config files—_quarto.yml(against the project-config schema) and_metadata.yml(directory metadata, against the frontmatter schema):- Type mismatches. A scalar whose resolved YAML 1.2 type does not match the schema (e.g. a boolean field set to a non-boolean) is flagged. String fields accept any scalar, since Quarto coerces.
- Invalid enum values. A scalar outside a field’s allowed set is flagged.
The Quarto schema version the rule validates against can be pinned with
[compat] quarto = "1.9". One version is currently bundled, so this is advisory.
Example violation:
---
toc: maybe
---Diagnostic:
warning[quarto-schema-type-mismatch]: value should be a boolean
--> document.qmd:2:6
|
2 | toc: maybe
| ^^^^^
Project config files are validated wherever the linter encounters them: the CLI checks an explicit panache lint _quarto.yml (or _metadata.yml) target, and the LSP validates the manifests reachable from an open project document, publishing diagnostics on each manifest’s own file.
Opt-out: Like Quarto itself, this rule (and quarto-schema-unknown-key) honors validate-yaml: false. When a document’s frontmatter or a manifest sets it, no schema validation runs for that file—useful for filter-driven patterns the core schema rejects, such as a bibliography: map for multibib. A malformed-YAML parse error is still reported.
Limitations: Format-gated keys are accepted regardless of the active output format, and deeply nested options reached through a permissive anyOf branch may not be checked.
quarto-schema-unknown-key
Flags unknown or misspelled keys in document frontmatter, code-cell options, and project config files, validated against Quarto’s machine-readable schema.
- Severity
- Warning
- Default
-
Off—opt in via
[lint.rules] quarto-schema-unknown-key = true. - Auto-fix
- No
- Diagnostic codes
-
quarto-schema-unknown-key - Requirements
-
Quarto flavor (
flavor = "quarto"). Pandoc has no metadata schema, so the rule does not run for other flavors. - Description
-
This rule is off by default because Quarto itself tolerates unknown keys at render time. Its schema objects are open so that pandoc passthrough and custom template metadata keep working. So flagging them is stricter than
quarto render. It is still useful for catching silent-failure typos (a misspelledformatblock simply does nothing, and Quarto will not warn), so it is offered as an opt-in.When enabled, on closed schema objects (e.g. a specific format’s option block) any undeclared key is flagged. On the open top level (which must allow pandoc passthrough and custom metadata) only a near-miss typo of a known key is flagged, with a “did you mean” suggestion.
Example violation (with the rule enabled):
---
forrmat: html
---Diagnostic:
warning[quarto-schema-unknown-key]: unknown key `forrmat`; did you mean `format`?
--> document.qmd:2:1
|
2 | forrmat: html
| ^^^^^^^
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 or 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).