Summary
The markdownlint hook's has_local_config branch drops --config entirely, so
a repo with its own .markdownlint.yaml gets none of the rule disables in
markdownlint-cli/.markdownlint.json — including MD060: false, which is set
there precisely because MD060 rejects the table style terraform-docs emits.
The two branches therefore disagree about the same file: with our global config
it passes, with a repo-local config it fails. --fix does not repair MD060, so
the commit is simply blocked.
Hit this on beacon-biosignals/infra. A one-line README edit failed the hook on
a terraform-docs-generated table 60 lines away from the change, in a file whose
style matches all 78 generated READMEs in that repo. Beacon's CI does not run
markdownlint at all, so nobody else on the team sees it.
This is the same shape as #290: a global hook default that fights a repo's own
convention, with an escape hatch that works but that nobody knows to reach for
until they've lost an hour.
Reproduction
d=$(mktemp -d) && cd "$d"
printf '%s\n' '# T' '' '| A | B |' '|------|------|' '| 1 | 2 |' > t.md
printf '%s\n' '{ "MD013": false }' > .markdownlint.json # any repo-local config
markdownlint --fix t.md # has_local_config branch
markdownlint --fix --config "$HOME/.config/markdownlint-cli/.markdownlint.json" t.md
The first command fails, the second passes:
t.md:4:8 error MD060/table-column-style [Table pipe does not align with header for style "aligned"]
t.md:4:15 error MD060/table-column-style [Table pipe does not align with header for style "aligned"]
exit=1
exit=0
Same binary (markdownlint-cli 0.49.1 via Homebrew), same file — the only
variable is whether --config is passed. A clean markdown file passes on the
local-config branch (exit=0), so the failure is MD060 specifically and not
the branch being broken in general.
Note that --fix leaves |------| untouched. MD060 blocks the commit without
offering a fix, so unlike the shfmt case in #290 there is no silent rewrite —
just a hard stop with no way through.
Where
pre-commit/config.yaml:124-132
if [[ "$has_local_config" -eq 1 ]]; then
markdownlint --fix "$@";
else
markdownlint --fix --config "$HOME/.config/markdownlint-cli/.markdownlint.json" "$@";
fi
The intent (from #116) is right: a repo with its own conventions should not have
our personal config forced onto it. The gap is that "defer to the repo" is
implemented as "use no config but the repo's," which silently also means "drop
every rule we globally decided is wrong" — MD060: false among them
(markdownlint-cli/.markdownlint.json:12).
MD060 is new enough that no repo-local config predating it mentions it. Beacon's
infra/.markdownlint.yaml configures nine rules and MD060 is not one of them,
so it runs at its default and objects to output the repo generates
mechanically.
Blast radius
beacon-biosignals/infra — 78 README.md files with the offending table
style, all 78 terraform-docs-generated. Any commit touching any of them
fails.
beacon-biosignals/platform-datastore — 2 files, local config, no MD060
entry. Same trap, not yet triggered.
smartwatermelon/personify — 1 file, and its local config does set MD060.
So this has already been rediscovered and worked around once, per-repo.
That last one is the argument for fixing it centrally: the workaround is being
reinvented in individual repos, including a Beacon repo where committing a
personal-tooling accommodation to shared config would be inappropriate.
Options
- Always pass our config, and let markdownlint's own discovery layer on
top. Ruled out by test: --config replaces repo-local discovery rather
than merging with it. With a repo config setting MD040: true and our global
config setting MD040: false, passing --config makes the file pass — the
repo's opinion is discarded, not merged. (Control: the same repo config alone
fails the file, so the rule does fire.) This is also why the current branch
exists at all, so it is not a candidate fix.
- Keep the branch, but pass our global config as the baseline via a
generated merge. Read both files, deep-merge repo-over-global, write to a
temp config, pass that. Correct semantics ("repo wins, our disables still
apply") but adds YAML/JSON merging to a bash hook.
- Move the genuinely universal disables out of the global config and into
hook flags that apply on both branches. MD060 is the only one that has
bitten so far. Smallest change; the rule stays off everywhere without
touching config discovery.
- Warn instead of failing when the only violations come from a repo-local
branch. Matches the flake8 reasoning already in this file (lines 45-46:
linting reports, it does not rewrite, so the worst case is a message). Would
have let the infra commit through with a note.
- Leave as-is and document it. The escape hatch works — add
MD060: false
to the repo's config. But for a Beacon repo that means committing a change
to shared config to accommodate one person's local hook, which is the wrong
place for it.
My preference is 3, falling back to 4. MD060's default assumes hand-written
tables; any repo generating markdown from a tool is a candidate, and
terraform-docs is common enough that this will recur.
Happy to send a PR for whichever you prefer.
Summary
The markdownlint hook's
has_local_configbranch drops--configentirely, soa repo with its own
.markdownlint.yamlgets none of the rule disables inmarkdownlint-cli/.markdownlint.json— includingMD060: false, which is setthere precisely because MD060 rejects the table style
terraform-docsemits.The two branches therefore disagree about the same file: with our global config
it passes, with a repo-local config it fails.
--fixdoes not repair MD060, sothe commit is simply blocked.
Hit this on
beacon-biosignals/infra. A one-line README edit failed the hook ona
terraform-docs-generated table 60 lines away from the change, in a file whosestyle matches all 78 generated READMEs in that repo. Beacon's CI does not run
markdownlint at all, so nobody else on the team sees it.
This is the same shape as #290: a global hook default that fights a repo's own
convention, with an escape hatch that works but that nobody knows to reach for
until they've lost an hour.
Reproduction
The first command fails, the second passes:
Same binary (markdownlint-cli 0.49.1 via Homebrew), same file — the only
variable is whether
--configis passed. A clean markdown file passes on thelocal-config branch (
exit=0), so the failure is MD060 specifically and notthe branch being broken in general.
Note that
--fixleaves|------|untouched. MD060 blocks the commit withoutoffering a fix, so unlike the shfmt case in #290 there is no silent rewrite —
just a hard stop with no way through.
Where
pre-commit/config.yaml:124-132The intent (from #116) is right: a repo with its own conventions should not have
our personal config forced onto it. The gap is that "defer to the repo" is
implemented as "use no config but the repo's," which silently also means "drop
every rule we globally decided is wrong" —
MD060: falseamong them(
markdownlint-cli/.markdownlint.json:12).MD060 is new enough that no repo-local config predating it mentions it. Beacon's
infra/.markdownlint.yamlconfigures nine rules and MD060 is not one of them,so it runs at its default and objects to output the repo generates
mechanically.
Blast radius
beacon-biosignals/infra— 78README.mdfiles with the offending tablestyle, all 78
terraform-docs-generated. Any commit touching any of themfails.
beacon-biosignals/platform-datastore— 2 files, local config, no MD060entry. Same trap, not yet triggered.
smartwatermelon/personify— 1 file, and its local config does set MD060.So this has already been rediscovered and worked around once, per-repo.
That last one is the argument for fixing it centrally: the workaround is being
reinvented in individual repos, including a Beacon repo where committing a
personal-tooling accommodation to shared config would be inappropriate.
Options
top. Ruled out by test:
--configreplaces repo-local discovery ratherthan merging with it. With a repo config setting
MD040: trueand our globalconfig setting
MD040: false, passing--configmakes the file pass — therepo's opinion is discarded, not merged. (Control: the same repo config alone
fails the file, so the rule does fire.) This is also why the current branch
exists at all, so it is not a candidate fix.
generated merge. Read both files, deep-merge repo-over-global, write to a
temp config, pass that. Correct semantics ("repo wins, our disables still
apply") but adds YAML/JSON merging to a bash hook.
hook flags that apply on both branches.
MD060is the only one that hasbitten so far. Smallest change; the rule stays off everywhere without
touching config discovery.
branch. Matches the flake8 reasoning already in this file (lines 45-46:
linting reports, it does not rewrite, so the worst case is a message). Would
have let the infra commit through with a note.
MD060: falseto the repo's config. But for a Beacon repo that means committing a change
to shared config to accommodate one person's local hook, which is the wrong
place for it.
My preference is 3, falling back to 4. MD060's default assumes hand-written
tables; any repo generating markdown from a tool is a candidate, and
terraform-docsis common enough that this will recur.Happy to send a PR for whichever you prefer.