Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration

mdt is configured through an mdt.toml file placed in the project root. Configuration is optional — mdt works without it using sensible defaults.

Creating a config file

Create mdt.toml in your project root:

[data]
package = "package.json"

[exclude]
patterns = ["vendor/", "dist/"]

What mdt init writes

mdt init creates a fully annotated starter mdt.toml so new projects can see every currently supported option before uncommenting anything.

# mdt.toml

# 

# Welcome to mdt. This starter config is intentionally fully annotated so you

# can discover every supported option in one place.

# 

# Uncomment only what your project needs. mdt works without a config file, but

# `mdt.toml` becomes useful once you want data interpolation, custom scanning

# rules, padding control, or formatter-aware convergence.

# 

# When in doubt, start with a sample template + target block, run `mdt update`,

# and then come back here to enable the options that match your workflow.

# Maximum file size (in bytes) that mdt will scan before failing fast.

# Leave this commented to use the built-in default of 10 MB.

# max_file_size = 10485760

# By default mdt respects `.gitignore` and skips ignored files.

# Uncomment this only when ignored/generated files should still be scanned, or

# when you want `[include]` / `[exclude]` to be your only scanning rules.

# disable_gitignore = true

# `[padding]` controls the whitespace between tags and injected content.

# Supported values for `before` and `after`:

# - false -> keep content inline with the tag

# - 0 -> put content on the next line with no blank line

# - 1 -> add one blank line

# - 2+ -> add two or more blank lines

# 

# Recommended when your targets live in source-code comments or when formatters

# tend to rewrite surrounding whitespace.

# [padding]

# before = 0

# after = 0

# `[check]` controls how `mdt check` compares expected vs actual content.

# - "strict" -> byte-for-byte comparison (default)

# - "lenient" -> whitespace-normalized comparison; ignores differences in

# blank line count, trailing whitespace, and table/JSON formatting so

# external formatters do not cause false staleness.

# `mdt update` always writes exact bytes regardless of this setting.

# [check]

# comparison = "lenient"

# `[data]` maps namespaces to external data sources. These values are available

# in source blocks through minijinja templates like `{{ package.version }}`.

# 

# String values are file-backed sources. The parser is inferred from the file

# extension (`.json`, `.toml`, `.yaml`, `.yml`, `.kdl`, `.ini`).

# [data]

# package = "package.json"

# cargo = "Cargo.toml"

# config = "config.yaml"

# 

# Typed data sources force a parser when the extension is missing or unusual.

# release = { path = "release-info", format = "json" }

# 

# Script-backed data sources run a shell command from the project root and parse

# stdout. `format` accepts: `text`, `string`, `raw`, `txt`, `json`, `toml`,

# `yaml`, `yml`, `kdl`, or `ini`.

# `watch` files control cache invalidation in `.mdt/cache/data-v1.json`.

# version = { command = "cat VERSION", format = "text", watch = ["VERSION"] }

# git = { command = "git rev-parse --short HEAD", format = "text" }

# `[exclude]` skips files, directories, or block names during scanning.

# `patterns` use gitignore-style syntax, including `!negation`, trailing `/`,

# `*`, `**`, and character classes.

# [exclude]

# patterns = ["vendor/", "dist/", "generated/", "!generated/keep.md"]

# 

# `markdown_codeblocks` only affects fenced code blocks inside source-file

# comments. Supported values:

# - false -> process tags in code blocks normally (default)

# - true -> ignore tags in all fenced code blocks

# - "..." -> ignore code blocks whose info string contains that text

# - ["...", ...] -> ignore code blocks matching any listed info-string text

# markdown_codeblocks = true

# markdown_codeblocks = "ignore"

# markdown_codeblocks = ["ignore", "example"]

# 

# `blocks` excludes specific block names everywhere, even if their files are

# still scanned.

# blocks = ["draftSection", "deprecatedApi"]

# `[include]` narrows scanning to only matching files. Use it when you want a

# smaller, more predictable scan surface in large repos.

# [include]

# patterns = ["docs/**/*.rs", "src/**/_.ts", "packages/_/readme.md"]

# `[templates]` restricts where `*.t.md` provider files are discovered.

# Leave it commented to allow template discovery anywhere in the project.

# [templates]

# paths = [".templates", "shared/templates"]

# `[[formatters]]` makes `mdt update` and `mdt check` converge with your

# formatter's canonical output.

# 

# This is the recommended fix when `mdt update`, your formatter, and

# `mdt check` would otherwise bounce back and forth in CI.

# 

# Formatter `command` values are rendered with minijinja before execution.

# Available variables:

# - `{{ filePath }}` -> absolute path to the file being formatted

# - `{{ relativeFilePath }}` -> path relative to the project root

# - `{{ rootDirectory }}` -> absolute path to the project root

# 

# `patterns` and `ignore` are both ordered gitignore-style rule lists. A

# leading `!` negates a prior match, so later rules can re-include paths.

# 

# Start with one catch-all formatter when your repo already uses a router like

# dprint. Add more entries when different file types need different tools.

# [[formatters]]

# command = "dprint fmt --stdin \"{{ filePath }}\""

# patterns = ["**/*.md"]

# ignore = ["**/*.t.md", "**/*.snap"]

# 

# [[formatters]]

# command = "prettier --stdin-filepath \"{{ filePath }}\""

# patterns = ["**/*.ts", "**/*.tsx"]

# ignore = ["dist/**"]

Sections

[data] — Data file mappings

Maps namespace names to data files. Each entry makes the file’s contents available as template variables under that namespace.

[data]
package = "package.json"
cargo = "Cargo.toml"
config = "config.yaml"

This creates three namespaces:

  • {{ package.name }} reads from package.json
  • {{ cargo.package.version }} reads from Cargo.toml
  • {{ config.database.host }} reads from config.yaml

Paths are relative to the project root (where mdt.toml lives).

Supported formats: JSON, TOML, YAML (.yaml/.yml), and KDL.

See Data Interpolation for full details.

[exclude] — Exclude patterns

Patterns for files and directories to skip during scanning. Uses gitignore-style syntax — the same pattern format as .gitignore files, including negation (!), directory markers (/), wildcards (*, **), and character classes.

[exclude]
patterns = [
  "vendor/",
  "dist/",
  "generated/",
  "**/*.generated.md",
  "!generated/keep-this.md",
]

These patterns are checked relative to the project root. In addition to your explicit patterns, mdt always skips hidden directories (.git, .vscode, etc.), node_modules/, and target/.

markdown_codeblocks — Skip tags in code blocks

Controls whether mdt tags inside fenced code blocks in source-file comments are processed. This is useful when doc comments contain fenced examples that show mdt tag syntax but should not be treated as real tags.

[exclude]
# Skip tags inside ALL fenced code blocks
markdown_codeblocks = true

# Skip only code blocks whose info string contains "ignore"
markdown_codeblocks = "ignore"

# Skip code blocks whose info string contains any of these
markdown_codeblocks = ["ignore", "example"]

The default is false, meaning tags in fenced source-comment code blocks are processed normally.

blocks — Exclude specific block names

Array of block names to exclude. Any block (source or target) whose name appears in this list is completely ignored.

[exclude]
blocks = ["draft-section", "deprecated-api"]

[include] — Include patterns

Restrict scanning to only files matching these patterns:

[include]
patterns = ["docs/**/*.rs", "src/**/*.ts"]

When set, only files matching at least one include pattern are scanned (in addition to markdown and template files which are always included).

[templates] — Template search paths

By default, mdt finds *.t.md files anywhere in the project. You can restrict where it looks:

[templates]
paths = ["templates", "shared/docs"]

When set, only *.t.md files within these directories are recognized as template files.

[padding] — Block content padding

Controls blank lines between block tags and their content. This is recommended when using target blocks in source code files.

[padding]
before = 0
after = 0

before and after accept false (inline), 0 (next line), 1 (one blank line), 2, etc. When [padding] is present but values are omitted, they default to 1. In source code files, blank lines use the same comment prefix as surrounding lines (e.g., //!, ///, *).

Without this setting, transformers like trim can cause content to merge directly into the surrounding tags, breaking the structure of code comments.

Recommended for projects with formatters: Use before = 0, after = 0 to minimize whitespace that formatters might alter.

[[formatters]] — Formatter-aware update/check pipeline

Formatter entries make mdt update and mdt check converge with your formatter’s canonical full-file output instead of comparing raw injected block text.

This is the recommended long-term fix for the mdt update → formatter → mdt check cycle described in issue #46, and the best way to keep CI green when external formatters rewrite synced files.

Each matching formatter entry:

  • reads the full candidate file from stdin
  • writes the full replacement file to stdout
  • runs from the project root
  • runs after block injection during mdt update
  • runs before expected-output comparison during mdt check
  • runs in declaration order when multiple entries match the same file

command is rendered with minijinja before execution. Available variables:

  • {{ filePath }} — absolute path to the file being formatted
  • {{ relativeFilePath }} — path relative to the project root
  • {{ rootDirectory }} — absolute project root

patterns and ignore are ordered gitignore-style rule lists. Leading ! entries negate a prior match, so later rules can re-include paths for a single formatter stage.

If a formatter command fails, exits non-zero, or renders an invalid minijinja command template, mdt returns an explicit formatter error instead of silently falling back to unformatted output.

[[formatters]]
command = "dprint fmt --stdin \"{{ filePath }}\""
patterns = ["**/*.md", "!docs/generated/**"]
ignore = ["vendor/**", "docs/generated/**", "!docs/generated/keep.md"]

Repositories without configured formatters keep the legacy fast path, so formatter support only adds work when you opt in.

max_file_size — Safety limit for scanned files

Set the maximum file size (in bytes) that mdt will scan. Files larger than this limit return an error.

max_file_size = 10485760 # 10 MB

If omitted, mdt uses a default of 10 MB.

disable_gitignore — Disable .gitignore integration

By default, mdt respects .gitignore rules when scanning for files, skipping anything that git would ignore. Set disable_gitignore = true to turn off this behavior:

disable_gitignore = true

When this option is enabled, mdt scans all files regardless of .gitignore rules. You can still control which files are scanned using the [exclude] and [include] sections.

When to use this:

  • Generated files with mdt blocks — If your build output or generated files contain target blocks that need updating, those files are typically listed in .gitignore but still need to be scanned by mdt.
  • Working outside a git repository — If the project is not a git repo, .gitignore resolution can cause unnecessary overhead or errors. Disabling it avoids those issues.
  • Full control over scanning — When you prefer to manage file inclusion/exclusion entirely through [exclude] and [include] patterns rather than relying on .gitignore.

If omitted, defaults to false (.gitignore rules are respected).

Sub-project boundaries

If mdt encounters a directory containing its own mdt.toml, it treats that directory as a separate project and skips it. This is useful in monorepos where each package manages its own templates:

my-monorepo/
  mdt.toml                    # root project config
  .templates/
    template.t.md
  packages/
    lib-a/
      mdt.toml                # lib-a is a separate mdt project
      .templates/
        template.t.md
    lib-b/
      mdt.toml                # lib-b is a separate mdt project
      .templates/
        template.t.md

Running mdt update from the root updates only the root project’s targets. Each sub-project is managed independently.

Annotated mdt.toml reference

The example below is synced from the repository’s annotated mdt.toml so the config reference and the real config evolve together.

# mdt.toml

# 

# This file is intentionally verbose: active entries show one working setup,

# and commented entries document every configuration option currently

# supported by the codebase.

# 

# Rule for contributors: when config behavior changes, update this annotated

# file and the synced configuration guide in the same PR.

# Top-level safety limit for scanned files, in bytes.

# Omit this to use the built-in default of 10 MB.

# Raise it for unusually large generated docs; lower it if you want earlier

# failure on oversized files.

# max_file_size = 10485760

# By default mdt respects `.gitignore` so it behaves like the repo itself.

# Set this to `true` only when ignored/generated files should still be scanned,

# or when you want `[include]` and `[exclude]` to be the only scanning rules.

# disable_gitignore = true

# Padding controls the blank lines between tags and injected content.

# Supported values for both `before` and `after`:

# - false -> keep content inline with the tag

# - 0 -> move content to the next line with no blank line

# - 1 -> one blank line

# - 2+ -> two or more blank lines

# 

# This repo uses `0`/`0` because it keeps comment-based targets formatter-stable

# without introducing extra blank lines for dprint/rustfmt to rewrite.

[padding] before = 0 after = 0

# `[check]` controls how `mdt check` compares expected vs actual content.

# - "strict" -> byte-for-byte comparison (default)

# - "lenient" -> whitespace-normalized comparison; ignores differences in

# blank line count, trailing whitespace, and table/JSON formatting so

# external formatters do not cause false staleness.

# 

# `mdt update` always writes exact bytes regardless of this setting.

# 

# This repo uses `lenient` so that dprint can reformat generated targets

# without tripping `mdt check`.

[check] comparison = "lenient"

[data]

# String values are file-backed namespaces.

# The parser is inferred from the extension: `.json`, `.toml`, `.yaml`,

# `.yml`, `.kdl`, and `.ini` are supported.

# 

# This repo exposes Cargo metadata as `{{ cargo.package.* }}` so templates can

# stay synchronized with workspace package information.

cargo = "Cargo.toml"

# Typed data sources let you force a parser when the extension is missing,

# unusual, or intentionally generic.

# release = { path = "release-info", format = "json" }

# Script-backed data sources shell out and parse stdout.

# `format` accepts: `text`, `string`, `raw`, `txt`, `json`, `toml`, `yaml`,

# `yml`, `kdl`, or `ini`.

# `watch` lists files that invalidate the cached result in

# `.mdt/cache/data-v1.json`.

# 

# Use this when the source of truth comes from tooling instead of a checked-in

# file.

# version = { command = "cat VERSION", format = "text", watch = ["VERSION"] }

# git = { command = "git rev-parse --short HEAD", format = "text" }

[exclude]

# Gitignore-style patterns skip files or directories during scanning.

# Supports `!negation`, trailing `/` for directories, `*`, `**`, and character

# classes.

# 

# This repo excludes test-only fixtures and snapshot directories so mdt only

# scans files that can contain real, maintained blocks.

patterns = [ "**/tests/", "**/__tests.rs", "**/snapshots/", ]

# `markdown_codeblocks` only affects fenced code blocks that appear inside

# source-file comments. It exists so docs/examples can show mdt tags without

# accidentally turning those examples into live targets.

# 

# Supported values:

# - false -> process tags in fenced code blocks normally (default)

# - true -> ignore tags in all fenced code blocks

# - "..." -> ignore code blocks whose info string contains that substring

# - ["...", ...] -> ignore code blocks whose info string matches any substring

# 

# This repo uses `true` because source-comment examples should stay

# illustrative, not executable.

markdown_codeblocks = true

# `blocks` excludes specific block names everywhere, even if their files are

# scanned. Use it when a block name is temporary, experimental, or

# intentionally unmanaged.

# blocks = ["draftSection", "experimentalApi"]

# `include` narrows scanning to only matching files. Use it to opt into a

# smaller search space in large repos once you know exactly where mdt tags live.

# [include]

# patterns = ["docs/**/*.rs", "src/**/_.ts", "packages/_/readme.md"]

# `templates.paths` restricts where `*.t.md` source files are discovered.

# Leave it unset to find template files anywhere in the project.

# Use it when a repo wants a dedicated source-of-truth directory layout.

# [templates]

# paths = [".templates", "shared/templates"]

# `[[formatters]]` lets `mdt update` and `mdt check` compare against your

# formatter's canonical output instead of raw injected text.

# 

# Formatter commands are rendered with minijinja before execution.

# Available variables:

# - `{{ filePath }}` -> absolute path to the file being formatted

# - `{{ relativeFilePath }}` -> path relative to the project root

# - `{{ rootDirectory }}` -> absolute path to the project root

# 

# `patterns` and `ignore` are both ordered rule lists with gitignore-like

# globs. A leading `!` negates a prior match, so later rules can re-include

# paths.

# 

# This repo enables dprint for generated markdown targets to prevent the

# formatter cycle from issue #46, where `mdt update` and `dprint fmt` would

# otherwise keep disagreeing in CI.

[[formatters]] command = "dprint fmt --stdin \"{{ filePath }}\"" patterns = ["**/*.md"] ignore = ["**/*.t.md"]

# Add more formatter stages when different file types need different tools.

# [[formatters]]

# command = "prettier --stdin-filepath \"{{ filePath }}\""

# patterns = ["**/*.ts", "**/*.tsx"]