hk-nix and mdformat gotchas
I run mdformat as a pre-commit hook via hk-nix and treefmt-nix. treefmt-nix runs over every markdown file in the tree and applies mdformat when it encounters .md files.
This is a collection of small gotchas.
So far I’ve just got two.
mdformat destroys YAML frontmatter by default ¶
Both SKILL.md files and Hugo static website content open with YAML frontmatter.
It might look like:
---
name: email-admin
description: >-
Administers MXroute email hosting through the mxroute MCP tools. [...]
allowed-tools:
- mcp__plugin_mxroute_mxroute__mxroute_list_domains
- mcp__plugin_mxroute_mxroute__mxroute_get_domain
license: MIT OR Apache-2.0
---
Or like:
date = '2026-09-27T21:31:10+02:00'
draft = false
title = 'hk-nix and mdformat gotchas'
tags = ['nix', 'rust']
Plain mdformat does not parse frontmatter. It reads --- as a long vertical line, +++ as literal
plus signs, YAML as a text paragraph, and special characters like _ as formatting.
This naturally breaks those files when they get interpreted by their target programs.
There is a plugin, mdformat-frontmatter, and treefmt-nix exposes mdformat’s plugin set:
# nix/treefmt.nix
programs.mdformat = {
enable = true;
plugins = ps: [ ps.mdformat-frontmatter ];
settings.number = true;
};
settings.number passes --number, which changes mdformat’s default is to renumber every ordered
list to all start with1.. This is great for some lists, but it might not be great for agent
skills. --number restores consecutive numbering, which is another way to ensure consistency.
cargo-readme and mdformat disagree about where link definitions go ¶
This one is a fight between two generators over the same file.
README.md in repos like hashpinner is generated from comments inside source code by
cargo-readme. The crate’s doc comments are spliced into a README.tpl at the point
where {{readme}} occurs. A pre-push hook checks that the committed file still matches what the generator
produces. An example of README.tpl:
# {{crate}}
You can download prebuilt [static binaries for x86_64 and aarch64 Linux][releases].
[releases]: https://github.com/sshine/hashpinner/releases
## CLI
{{readme}}
## The CI Action
[...]
mdformat collects markdown link reference definitions and moves them to the end of the document. So
the pre-commit hook takes that [releases]: line out of the middle of the file and parks it on the
last line of README.md, 200 lines below where the template puts it. The pre-push hook then
regenerates from the template, puts it back, and the next commit moves it down again. The two hooks
are both correct, but they undo each other forever.
A fix is to stop comparing raw generator output, and pipe one generator into the other:
# nix/hooks.nix
# cargo-readme emits markdown that mdformat then rewrites (link reference
# definitions move to the end of the file), so the raw output never equals the
# committed README.md and the two hooks would undo each other forever.
mdformat = config.treefmt.settings.formatter.mdformat.command;
readme = "${cargo-readme} ${readmeArgs} | ${mdformat} -";
TODO: readmeArgs above isn’t listed in the code above, while it’s part of the solution. And the readme below overlaps with the one above. They’re listed in the justfile as readme_args, not sure if there’s an overlap or I’m repeating myself. If I’m repeating myself, don’t bother DRY’ing them or even mentioning the DRY.
readme = {
check = "${readme} | diff - README.md";
fix = "${readme} > README.md";
};
config.treefmt.settings.formatter.mdformat.command is the binary treefmt-nix already built for the
pre-commit hook. Pulling it from there makes the pipeline agree with the formatter by construction:
the plugins and --number above are part of this mdformat too, so the README is generated by
exactly the tool that will later reformat it.
The same pipeline goes in the justfile, so regenerating by hand gets the same output as the hook:
readme_args := "--project-root crates/hashpinner --input src/main.rs --template ../../README.tpl"
# Regenerate README.md from README.tpl and the CLI docs
readme:
cargo readme {{readme_args}} | mdformat - > README.md
# Check README.md is in sync with README.tpl and the CLI docs
readme-check:
cargo readme {{readme_args}} | mdformat - | diff - README.md