Claude Codeでmarkdownlint-cli2を回す — 編集のたびに規約違反を直させる設定
markdownlint-cli2を編集直後のPostToolUse hookに載せ、見出し階層や空行、コードフェンスの言語指定をClaude自身に直させる設定です。設定ファイルの書き方と、--fixを任せる範囲の決め方をまとめます。
markdownlint-cli2は、Markdownの書き方を規則で検査するコマンドラインツールです。中身のライブラリはmarkdownlintで、見出しの階層、空行の入れ方、コードフェンスの言語指定、行末の空白といった体裁を、規則番号(MD001など)付きで指摘します。
Claude CodeにREADMEや設計メモを書かせると、見出しが1段飛んだり、コードフェンスに言語が付かなかったりといった揺れが出ます。内容に誤りはなくても、レビューで毎回同じ指摘をするのは手間です。PostToolUse hookで編集直後に検査を走らせれば、指摘をClaude自身が受け取って直せます。
この記事は、設定ファイルの置き方、Claudeが書く文書で引っかかりやすい規則、hookの組み方、--fixに任せる範囲の決め方の順で進めます。日本語の文章そのものの校正はClaude Codeでtextlintを使うの領分で、ここで扱うのは構文と体裁です。
インストールと最初の実行
プロジェクトの開発依存に入れると、チーム全員とhookが同じバージョンを使えます。グローバルに入れる方法やHomebrewも用意されています。
npm install markdownlint-cli2 --save-dev
npx markdownlint-cli2 "**/*.md" "#node_modules"引数は検査対象のglob(ファイル名のパターン)です。先頭が!か#のパターンは除外を表します。シェルによっては!をダブルクォート内で解釈するため、公式は#を使う書き方を最も互換性が高い形として示しています。
markdownlint-cli2 .と書くと、カレントディレクトリの*.{md,markdown}だけが対象になります。ツリー全体を見たいときは"**"を渡します。
終了コードは3種類です。
| 終了コード | 意味 |
|---|---|
| 0 | 意味検査は成功し、エラーなし(警告はあってもよい) |
| 1 | 意味検査は成功し、エラーあり |
| 2 | 意味問題や障害のため検査できなかった |
指摘の一覧は既定で標準エラー出力(stderr)に書かれます。この性質は、後述のhookで出力を集めるときに効いてきます。
.markdownlint-cli2.jsoncで規則を絞る
設定ファイルは、プロジェクトのルートに置きます。形式は複数あり、.markdownlint-cli2.jsonc、.markdownlint-cli2.yaml、.markdownlint-cli2.cjsまたは.mjsの系統が、ツールの挙動全体を制御します。規則の設定だけを書く.markdownlint.jsoncや.markdownlint.yamlの系統もあり、こちらはmarkdownlint-cliなど他のツールからも読まれます。同じディレクトリに複数あるときは、前者なら.jsonc、.yaml、.cjs、.mjsの順で1つだけが使われます。
最小の例は次のとおりです。
{
"config": {
"default": true,
"MD013": false,
"MD024": { "siblings_only": true },
"MD033": { "allowed_elements": ["details", "summary", "br"] }
},
"gitignore": true,
"noBanner": true,
"noProgress": true,
"globs": ["**/*.md"]
}configの中身は、markdownlintの設定オブジェクトです。defaultを先に適用し、そのあとキーを上から順に処理します。規則は番号(MD013)でも別名(line-length)でも、タグ(whitespaceなど)でも指定でき、falseで無効になります。
トップレベルのキーのうち、hookと相性がよいものを挙げます。
| キー | 働き |
|---|---|
globs | 働き検査対象のglobを設定ファイル側に持たせる(コマンドラインに足される) |
gitignore | 働きtrueで.gitignoreに載ったファイルを除外する |
ignores | 働き除外するglobを列挙する |
noBanner / noProgress | 働きバナーと進捗の表示を止める。両方をtrueにすると標準出力に何も出ない |
fix | 働きtrueで、修正情報を持つ規則の指摘を自動で直す |
overrides | 働きfilterに合致したファイルだけ別のconfigを当てる。combineはmergeかreplace |
noBannerとnoProgressは、トップレベルの設定で、コマンドを実行するディレクトリでだけ有効です。hookの出力はそのままClaudeの文脈に入るので、バナーや進捗の行を止めておくと、返ってくるのは指摘の行だけになります。
ディレクトリごとに設定を変えることもできます。サブディレクトリに別の設定ファイルを置くと、上位の設定に重なって適用されます。たとえばdocs/だけMD013を有効にする、CHANGELOG.mdだけ見出し重複を許す、といった調整が、overridesを使わなくても書けます。
Claudeが書く文書で引っかかりやすい規則
markdownlintの規則はMD001からMD060まで50本ほどあり、既定ではすべて有効です。Claudeに書かせた文書では、次の規則が出やすい部類に入ります。どれを残すかは、プロジェクトの文書の書き方で決まります。
| 規則 | 指摘の内容 | 自動修正 |
|---|---|---|
| MD001 | 指摘の内容見出しのレベルが1段ずつ増えていない | 自動修正なし |
| MD013 | 指摘の内容行が長い(既定は80文字) | 自動修正なし |
| MD022 | 指摘の内容見出しの前後に空行がない | 自動修正あり |
| MD024 | 指摘の内容同じ文言の見出しが複数ある | 自動修正なし |
| MD025 | 指摘の内容最上位の見出しが複数ある | 自動修正なし |
| MD031 | 指摘の内容コードフェンスの前後に空行がない | 自動修正あり |
| MD032 | 指摘の内容箇条書きの前後に空行がない | 自動修正あり |
| MD033 | 指摘の内容生のHTMLを使っている | 自動修正なし |
| MD034 | 指摘の内容URLが山括弧で囲まれていない | 自動修正あり |
| MD040 | 指摘の内容コードフェンスに言語指定がない | 自動修正なし |
| MD041 | 指摘の内容ファイルの先頭行が最上位の見出しでない | 自動修正なし |
自動修正の列は、規則の説明ページの「Fixable」の記載に従っています。「あり」の規則も、公式の表現は「一部の違反は修正できる」で、すべての指摘が直るとは限りません。
規則ごとに押さえておく点が少しあります。
MD013とMD041は設定で調整する
MD013(行の長さ)は、見出しにも表にもコードブロックにも別々の長さを設定できます。line_length、heading_line_length、code_block_line_lengthがそれぞれ既定で80文字です。
規則の説明には例外も書かれています。上限を超えた先に空白がなければ、長いURLなどを許すために報告されません。strictをtrueにするとこの例外が外れ、sternをtrueにすると、直せる長さ超過だけを警告し、空白のない長い行は許します。日本語の地の文は空白を含まないことが多いので、この例外がそのまま効く場面もあれば、英単語やコマンドが混じる行だけが引っかかる場面もあります。日本語の文書では、"MD013": falseで切るか、line_lengthを大きく取るかを決めておきます。
MD041は、先頭行を最上位の見出しにする規則です。ただしフロントマターにtitle:があれば、それを見出しの代わりに数えます。この判定はfront_matter_titleの正規表現(既定は^\s*title\s*[:=])で変わり、空文字を渡すとフロントマターの利用自体を止められます。ブログ記事のようにフロントマターで題名を持つ文書は、既定のままで通ります。
MD024とMD033は、文書の形に合わせて緩める
MD024(見出しの重複)はsiblings_onlyをtrueにすると、同じ親の下にある兄弟の見出しだけを比べます。「注意点」という見出しを節ごとに使いたい文書では、この設定が合います。
MD033(生のHTML)は、allowed_elementsに許可する要素名を並べます。<details>や<br>を使う文書では、あらかじめ書いておかないと、Claudeが折りたたみを書くたびに指摘が返ります。
PostToolUse hookで指摘を返す
PostToolUse hookは、ツールの呼び出しが成功した直後に動きます。matcherにEdit|Writeを指定すると、ファイルの書き換えのたびに起動できます。hookが受け取るJSONにはtool_input.file_pathが入るので、.mdと.markdownだけを対象にします。
.claude/settings.jsonには次のように書きます。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/markdownlint.sh",
"timeout": 30
}
]
}
]
}
}スクリプトは、検査結果が終了コード1(エラーあり)のときだけ、出力をstderrに流して自分も終了コード2で終えます。
mkdir -p .claude/hooks
cat > .claude/hooks/markdownlint.sh <<'SH'
#!/bin/bash
file_path=$(jq -r '.tool_input.file_path // empty')
[ -z "$file_path" ] && exit 0
case "$file_path" in
*.md|*.markdown) ;;
*) exit 0 ;;
esac
[ -f "$file_path" ] || exit 0
cd "${CLAUDE_PROJECT_DIR:-.}" || exit 0
bin=./node_modules/.bin/markdownlint-cli2
[ -x "$bin" ] || bin=$(command -v markdownlint-cli2) || exit 0
output=$("$bin" "$file_path" 2>&1)
status=$?
if [ "$status" -eq 1 ]; then
echo "$output" >&2
exit 2
fi
exit 0
SH
chmod +x .claude/hooks/markdownlint.sh終了コード2を返す理由は、公式のhooksリファレンスにあります。終了コード0で終えたhookのstderrはClaudeに届きません。PostToolUseでは、実行済みのツールを取り消せない代わりに、終了コード2のstderrがClaudeに渡されます。書き込みは済んでいるので、返るのは「次に直してほしい指摘」です。
cdでプロジェクトのルートに移っているのは、globが現在のディレクトリ配下のファイルに対して働くためです。
検査ツール自体が失敗した終了コード2は、スクリプトの最後で0に落としています。設定ファイルの記述ミスのようにClaudeが直せない問題を、Claudeに返しても堂々巡りになるからです。そのぶんツールの故障には気づきにくくなります。導入時に、わざと規則違反の.mdをClaudeに書かせて、指摘が返ることを一度見ておくと安心です。
hookの一般的な書き方と他のイベントとの違いはPostToolUse hookでツール実行後の後処理を自動化するにあります。同じ型をシェルスクリプトに当てた例はClaude CodeでShellCheckを使う、言語ごとのコマンドを並べた表はPostToolUse hookのLint自動修正です。
--fixを任せる範囲を決める
markdownlint-cli2は、--fixを付けるか、設定ファイルのfixをtrueにすると、直せる指摘をファイルに直接書き込みます。バックアップは作られません。ここがhookに載せるときの分かれ道です。
hookに組み込む形は、大きく2つあります。
- 検査だけ: 上のスクリプトのまま。指摘はすべてClaudeに返り、直すのはClaude
- 直せるものは直す:
--fixでファイルを整えたうえで、直らなかった指摘だけをClaudeに返す
後者のスクリプトは、検査の前に--fixを1回挟みます。
# 直せる指摘を先に書き換え、残りを検査してClaudeに返す
"$bin" --fix "$file_path" >/dev/null 2>&1
output=$("$bin" "$file_path" 2>&1)
status=$?空行の不足やURLの山括弧のような機械的な指摘は、Claudeに返すより先に直ったほうが、やり取りの往復が減ります。残るのは、見出し階層や言語指定の抜けのように、書き手の判断が要るものだけです。
ただし--fixは、有効になっている規則のうち修正情報を持つものすべてに効きます。任せてよい範囲を決めるときの目安として、規則を3つに分けて考えます。
| 区分 | 該当する規則の例 | 方針 |
|---|---|---|
| 任せてよい | 該当する規則の例MD009(行末の空白)、MD012(連続する空行)、MD022・MD031・MD032(空行の補い)、MD047(末尾の改行) | 方針意味を変えない整形なので--fixで直す |
| 差分を見たい | 該当する規則の例MD004(箇条書きの記号)、MD029(番号付きリストの番号)、MD049・MD050(強調の記号) | 方針文書全体の見た目が変わる。スタイルを設定で固定してから任せる |
| 任せない | 該当する規則の例MD044(固有名詞の大文字小文字)、MD051(リンクの断片)、MD053(使われない参照定義) | 方針内容や参照に触れる。報告だけにして人かClaudeが判断する |
落とし穴
MD044はコードブロックの中身まで書き換える
MD044は、namesに並べた固有名詞の大文字小文字をそろえる規則です。code_blocksとhtml_elementsの既定値はどちらもtrueなので、コードブロックやHTML要素の中も対象になります。namesにGitHubを入れた状態で--fixを回すと、コード例に含まれるgithub.comのような文字列も、そろえる対象にされます。github.comのように別の綴りも正しいものは、namesに両方の形を並べます。コード例まで触らせたくなければ、code_blocksをfalseにします。
「任せない」に入れた規則を無効にするには、設定のconfigで"MD044": falseのように書きます。規則を有効にしたまま--fixの対象からだけ外したいときは、規則そのものを切るか、--fixを使わない検査だけの形にするかの二択になります。
--fixを使うなら、gitで管理しているファイルに限ると安全です。書き込みが想定外でもgit diffで見えますし、差分を取り消せます。バックアップが作られない仕様を、版管理で補う形です。
標準入力で整形するモード(--format)
エディタの整形機能向けに、--formatも用意されています。標準入力を読み、修正を適用し、結果を標準出力に書きます。globは無視され、--fixが暗黙に有効になり、終了コード1も使われません。ファイルを直接書き換えたくない場面では、この形で整形後の文字列だけを受け取る使い方が考えられます。
指摘を消す近道を塞ぐ
指摘が返ってきたとき、最短で緑にする方法は、規則を無効にすることです。markdownlintには、Markdownの中に書くコメントで規則を止める仕組みがあります。
<!-- markdownlint-disable-next-line MD033 -->
<kbd>Ctrl</kbd> + <kbd>C</kbd>用意されているコメントは、disable(以降を無効)、disable-line(その行)、disable-next-line(次の行)、disable-file(ファイル全体)などです。規則は番号か別名で指定します。設定ファイルのnoInlineConfigをtrueにすると、これらのコメントの解釈自体が止まります。
近道を完全に塞ぎたければnoInlineConfigを使えますが、本当に意図した例外まで書けなくなります。近道を塞ぐ手段としては、CLAUDE.mdで運用を縛る方法もあります。
## Markdownの書き方
- .mdを編集したあとはmarkdownlint-cli2の指摘に従って直す
- 指摘は原則として文書の側を直す。`markdownlint-disable` は、
意図した例外に限り、直前に理由を書いたうえで1行だけ使う
- 設定ファイル(.markdownlint-cli2.jsonc)は、頼まれない限り編集しないhookが機械的に検査し、CLAUDE.mdが「どう直すか」の方針を持つ分担です。設定ファイルの編集禁止まで書いておくのは、規則を緩めて通す手口を防ぐためです。さらに固くするなら、.claude/settings.jsonのpermissions.denyで、設定ファイルへのEditを拒否する手もあります。
効かないケースと落とし穴
- Bashツール経由の書き込み:
Edit|Writeのhookは、Bashのコマンドで生成した.mdには動きません。cat > README.md <<EOFのような生成は、Claudeに明示的な検査コマンドを実行させる必要があります - .mdxは対象外にする: ドットだけのglobは
*.{md,markdown}に置き換わる仕様で、.mdxへの言及はありません。JSXのタグはMD033に引っかかるため、hookの拡張子判定も.mdと.markdownに絞っています - フロントマターの扱い: 文書の先頭にあるフロントマターは、ほとんどの規則で無視されます。独自の書式なら、
frontMatterにRegExpを渡して教えます - globの引用符: シェルがglobを展開すると、除外パターンの
!が効きません。コマンドでは引用符で囲みます - 設定の探索: 設定ファイルは置いたディレクトリとその配下に効き、サブディレクトリの設定が上位の設定に重なります。docsだけ別のルールにしたいときは、そのディレクトリに設定を置きます
- バージョンの固定: 公式のDockerとpre-commitの例は、
v0.23.3のようにバージョンを固定して書かれています。新しいバージョンが新しい規則や挙動を足すと、hookの結果が手元とずれるので、package.jsonのロックを共有するか、CIでも同じバージョンを使います
pre-commitを使うリポジトリなら、.pre-commit-config.yamlにmarkdownlint-cli2のフックを足して、コミット時の砦を重ねることもできます。環境のNode.jsのバージョンによっては、pre-commit側で上書きが要るとされています。
まとめ
markdownlint-cli2をPostToolUse hookに載せると、見出し階層や言語指定の抜けのように、毎回レビューで指摘していた体裁の揺れが、書いた直後にClaudeへ返ります。検査の結果は終了コード1ならエラーあり、とだけ覚えれば足り、hookは1のときだけ2で返す作りにしておけます。
設計の肝は2点です。1つは、Claudeに返す指摘を、MD013やMD033のようにプロジェクトの文書に合わない規則を先に調整してから決めること。もう1つは、--fixが書き込みをバックアップなしで行うため、MD044のように内容に触れる規則を修正の対象に置かないことです。最初は検査だけで始め、返ってくる指摘の中身を見て、機械的な整形系の規則から順に--fixへ移すのが無理のない進め方です。