Claude Codeでtextlintを使う — 日本語の校正をhookで自動化
textlintを編集直後のPostToolUse hookに載せ、日本語の表記ゆれや読みにくい文をClaude自身に直させる設定です。prhの辞書、--fixの使い分け、終了コードの注意点まで扱います。
textlintをhookに載せると何が変わるか
textlintは、Markdownやテキストの文章を、ルールのプラグインで検査するツールです。ルールは1つずつ独立したパッケージで、必要なものだけを足して使います。
これをClaude CodeのPostToolUse hookに入れると、Claudeが.mdファイルを書くたびに校正が走ります。表記ゆれのように機械的に直せるものは--fixが書き換え、読みにくい文のように判断が要るものはClaudeに返ります。人が後から赤を入れる前に、Claude自身が文章を直します。
ただし、導入しただけでは返ってくる指摘が多すぎる、あるいは何も返らない、のどちらかになりがちです。そこで次の順に組みます。
- textlintを入れて設定ファイルを置く
- プリセットが何を検出するかを知る
- 表記ゆれを
prhの辞書で固定する - PostToolUse hookで指摘をClaudeに返す
- 現場に合わせて緩める・除外する
言語ごとのLint自動修正の一覧はPostToolUse hookのLint自動修正の早見表にあります。ここではtextlintに絞ります。
textlintを入れて.textlintrc.jsonを置く
公式の手順は、作業ディレクトリにpackage.jsonを作り、textlintをローカルにインストールする形です。--save-devを付けるのが推奨で、ルールもローカルに入れる必要があります。
npm init --yes
npm install --save-dev textlint
npm install --save-dev \
textlint-rule-preset-ja-technical-writing \
textlint-rule-preset-ja-spacing \
textlint-rule-prh手元で上の4パッケージを入れたところ、textlint --versionはv15.8.0でした。プリセットはtextlint-rule-preset-ja-technical-writing@12.0.2とtextlint-rule-preset-ja-spacing@3.0.3、prhルールは6.1.0が入りました。
設定は.textlintrc.jsonに書きます。ファイルがワークスペースにあれば、コマンドラインのオプションなしで読み込まれます。npx textlint --initで雛形を作れます。日本語の技術文書向けの最小構成は次のとおりです。
{
"rules": {
"preset-ja-technical-writing": true,
"preset-ja-spacing": true,
"prh": { "rulePaths": ["./prh.yml"] }
}
}プリセットは名前の頭のtextlint-rule-を省いて書けます。trueが有効、falseが無効で、オプションは値をオブジェクトで渡します。
日本語プリセットは何を検出するか
preset-ja-technical-writingは、技術文書向けのルールをまとめたプリセットです。README自身が「全体的に少し厳しめの設定がデフォルト」と書いています。ルールの一覧から、hookで返す価値が高いものを選ぶと次のようになります。
| 検出するもの | 既定の基準 |
|---|---|
| 1文の長さ | 既定の基準100文字以下 |
| 読点の数 | 既定の基準1文に3つまで |
| 連続する漢字 | 既定の基準6文字まで |
| ですます調・である調の混在 | 既定の基準本文はですます、箇条書きはである |
| 逆接の「が」の連続 | 既定の基準同じ文中に複数回で指摘 |
| 同じ助詞の連続 | 既定の基準同じ文中で近接すると指摘 |
| ら抜き言葉・二重否定 | 既定の基準検出する |
| 文末の「。」の抜け | 既定の基準検出する |
preset-ja-spacingは、日本語と半角文字の間にスペースを入れるかどうかなどを決めるプリセットです。READMEによると、半角と全角の間は既定でスペースを入れません。
手元で確かめた指摘の出方
次のような短い文章をdocs/sample.mdに置いて、上の設定で実行しました。
# サンプル
サーバの設定を変更しました。ユーザに通知します。ClaudeCodeで確認できる。
この機能はファイルを読み込むことができるのですが、設定によってはエラーが出ることがありますが、その場合は再起動してください。npx textlint docs/sample.mdの結果は、5件の指摘のうち3件が自動修正可能でした。終了コードは1です。3件はprhの表記ゆれ(サーバ→サーバー、ユーザ→ユーザー、ClaudeCode→Claude Code)で、残る2件は5行目の逆接の「が」と同じ助詞の連続です。
この結果から、役割が分かれます。表記ゆれは辞書で機械的に直せます。文の組み立ては書き手が判断するので、指摘が出るだけです。hookでは前者を--fixに任せ、後者だけをClaudeに返す設計になります。
プリセットの残りのルールは、hookで返すものと切るものに分ける
READMEの目次に載るルールは23項目あります。上の表で触れたのはその一部です。残りは、指摘が出たときに直し方が一つに決まるものと、文体の好みで分かれるものに分けると、hookに載せる判断がしやすくなります。
| 分類 | ルール |
|---|---|
| 直し方が一つに決まる | ルールUTF8-MAC濁点、不必要な制御文字、ゼロ幅スペース、半角カナ、不自然なアルファベット、対になっていない括弧、同一単語の連続 |
| 文体の好みで分かれる | ルール弱い日本語表現、感嘆符・疑問符、漢数字と算用数字、冗長な表現、よくある誤用 |
前者は、入力ミスや貼り付け由来の文字化けを拾うルールです。出たら直すしかないので、hookでそのままClaudeに返して構いません。
後者は書き手の方針しだいです。たとえば弱い日本語表現は、〜かもしれないや〜と思いますを指摘します。事実が確定していない箇所をあえて控えめに書く文章では、指摘に従って直すと断定に変わります。
次の文章で、両方の挙動を見ました。
この設定は問題ないかもしれません。ファイルを読み込むことができます。テスト(括弧が閉じない。すごい!この文と、ゼロ幅スペースを入れた別の行を、上と同じ設定で検査しました。5件の指摘が出て、内訳は弱い表現のかも、半角カナ、閉じていない括弧、感嘆符、ゼロ幅スペースです。ファイルを読み込むことができますは指摘されませんでした。
文体で分かれるルールを切るには、プリセットの下でfalseを渡します。
{
"rules": {
"preset-ja-technical-writing": {
"ja-no-weak-phrase": false,
"no-exclamation-question-mark": false
}
}
}この2つを切ると、同じ文章の指摘は3件に減り、半角カナ、閉じていない括弧、ゼロ幅スペースが残りました。
括弧の指摘は、unix形式でも1件が4行にまたがりました。hookのhead -n 40は件数でなく行数で切るので、この種の指摘が重なると、40行目で後ろの指摘が途切れることがあります。
表記ゆれはprhの辞書で固定する
prhは、YAMLの辞書に書いた表記を検査するルールです。READMEによると--fixに対応しています。辞書はrulePathsで指定し、expectedが正しい表記、patternが検出したい誤表記です。
version: 1
rules:
- expected: サーバー
pattern: /サーバ(?!ー)/
- expected: ユーザー
pattern: /ユーザ(?!ー)/
- expected: Claude Code
pattern: /ClaudeCode|クロードコード/先頭の2つは、末尾の長音を付けない書き方だけを拾うために、否定先読み(?!ー)を付けています。付けないとサーバーのサーバにもマッチします。これは正規表現の一般的な書き方で、prh固有の機能ではありません。patternを省くとexpectedから表記ゆれのパターンが作られますが、日本語の長音のような規則は自分で書くほうが確実です。
辞書の各項目には、specsで「この入力はこう直る」というテストを書けます。READMEによると、specsが失敗するとprh.ymlの読み込み自体が失敗します。正規表現を足すたびに、サーバの設定がサーバーの設定になる、サーバーはそのまま、といった期待を残しておけば、否定先読みの書き間違いを実行前に検出できます。ほかの辞書はimportsで取り込めるので、チーム共通の辞書と、プロジェクト固有の辞書を分けて持つこともできます。
辞書に書く語は、チームで揃えたいものを足していく形が現実的です。Claudeが同じ誤表記を繰り返すなら、その1語を辞書に足せば、以降は--fixが直します。CLAUDE.mdに「サーバーと書く」と書くより、辞書のほうが確実に効きます。CLAUDE.mdはClaudeが読む文脈で、守られる保証はないためです。
コードブロックの中はどこまで検査されるか
textlint 15.8.0とprh 6.1.0の組み合わせでは、```bashの中のecho サーバと、インラインコードの`ユーザ`は、指摘されず--fixでも書き換わりませんでした。
prhにはcheckCodeCommentというオプションがあります。言語名の配列を渡すと、その言語のコードブロックのコメントを検査します。READMEによると、対応するのはJavaScriptのコードブロックだけです。
{
"rules": {
"prh": {
"rulePaths": ["./prh.yml"],
"checkCodeComment": ["js"]
}
}
}この設定で、// サーバの設定を読むというコメントとconst x = "ユーザ";という文字列リテラルを含むjsブロックを検査しました。指摘されて--fixで書き換わったのはコメントのサーバだけで、文字列リテラルのユーザはそのままでした。設定しなければ、このコメントは検査されません。
同じREADMEのcheckEmphasisは、強調(Emphasisノード)を検査するかどうかのオプションで、既定はfalseです。**ユーザの設定**のような太字は、このオプションを入れなくても本文の一部として指摘され、--fixで直りました。
PostToolUse hookで指摘を返す
hookは.claude/settings.jsonに書きます。Edit|Writeにマッチさせ、実体はスクリプトに逃がします。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/textlint.sh",
"timeout": 60
}
]
}
]
}
}PostToolUseの入力はJSONで、編集したファイルの絶対パスがtool_input.file_pathに入ります。スクリプトはこれを読み、Markdownとテキスト以外なら何もせず終わります。
mkdir -p .claude/hooks
cat > .claude/hooks/textlint.sh <<'SH'
#!/bin/bash
file_path=$(jq -r '.tool_input.file_path // empty')
case "$file_path" in
*.md|*.txt) ;;
*) exit 0 ;;
esac
cd "$CLAUDE_PROJECT_DIR" || exit 1
npx --no-install textlint --fix "$file_path" >/dev/null 2>&1
output=$(npx --no-install textlint --format unix "$file_path" 2>&1)
if [ $? -ne 0 ]; then
echo "$output" | head -n 40 >&2
exit 2
fi
exit 0
SH
chmod +x .claude/hooks/textlint.sh流れは3段です。まず--fixで直せる分を書き換え、次に残りを--format unixで検査し、指摘があれば終了コード2で返します。--format unixは1指摘がファイル:行:列: メッセージ [Error/ルール名]の1行にまとまるので、Claudeが読みやすい形式です。head -n 40は、助詞の連続のように説明が長いルールで出力が膨らむのを抑えるためです。
なぜ終了コード2なのか
PostToolUseはツールが成功した後に走るため、終了コード2でも編集は取り消せません。それでも、stderrの内容はClaudeに渡ります。公式のhooksリファレンスは、PostToolUseの警告をClaudeに見せたいときは終了コード2を使うよう書いています。終了コード0で返したstderrは、Claudeには見えません。
textlint自体の終了コードは、0が指摘なし、1が指摘あり、2が致命的エラーです。--fixで全件直せたときは0になります。スクリプトは非ゼロをまとめて2に変えているので、設定ファイルの読み込み失敗のような実行自体の失敗も、Claudeに伝わります。hookの終了コードの全般的な扱いはPostToolUse hookでツール実行後の後処理を自動化するにあります。
手元での動作確認
上のスクリプトに、Claude Codeが渡す形のJSONを直接流して動かしました。
export CLAUDE_PROJECT_DIR=$PWD
echo '{"tool_input":{"file_path":"'$PWD'/docs/sample.md"}}' \
| .claude/hooks/textlint.sh
echo "exit=$?"docs/sample.mdは、--fixで3行目がサーバーの設定を変更しました。ユーザーに通知します。Claude Codeで確認できる。に書き換わりました。標準エラー出力には5行目の2件だけが残り、終了コードは2でした。.goファイルを渡すと何も出さず0、指摘のない.mdでも0です。
現場に合わせて緩める・除外する
既定値は厳しめなので、そのまま入れるとClaudeが同じ指摘を何度も直し続けることがあります。調整の手段は3つあります。
ルールのオプションを変える。 プリセットの下に値を書くと、既定が変わります。たとえば文の長さを120文字に緩め、ですます調の検査を切る例です。
{
"rules": {
"preset-ja-technical-writing": {
"sentence-length": { "max": 120 },
"no-mix-dearu-desumasu": false
}
}
}除外するファイルを決める。 プロジェクト直下の.textlintignoreに、globパターンで書きます。手元では、.textlintignoreに載せたファイルを渡しても何も出力されず、終了コードは0でした。生成物や翻訳元の文書は、ここに入れておくとhookが黙ります。
範囲だけ無効にする。 <!-- textlint-disable -->と<!-- textlint-enable -->で囲んだ範囲を除外できます。これには設定で"filters": { "comments": true }を有効にする必要があります。手元で、フィルタを設定せずにこのコメントを書いたところ、囲んだ範囲の指摘は消えませんでした。
警告にしても、Claudeには届かない
ルールごとにseverityを指定できます。既定はerrorで、warningにすると終了コードは0のままです。上のスクリプトは終了コードが0なら黙るので、warningにした指摘はClaudeに返りません。「気づかせたいが止めたくはない」ルールをwarningにしても、このスクリプトでは効きません。Claudeに返したいルールはerrorのままにします。
標準で検査できるのはMarkdownとテキストだけ
textlintが標準で持つプラグインは、テキストとMarkdownの2つです。ほかの形式は、プラグインを入れて対応させます。手元で.mdxファイルを渡すと、検査対象にならず、出力も終了コードも「指摘なし」と同じ扱いでした。MDXの記事を書く環境では、校正が効いているように見えて実際は何も検査していない状態になります。スクリプトのcaseを*.md|*.txtにしているのはこのためです。
.mdxを検査対象にする
.mdxを足すには、MDX対応のプラグインを入れて、設定のpluginsに名前を書きます。npmで見つかるtextlint-plugin-mdxの1.1.0を、textlint 15.8.0と組み合わせて試しました。
npm install --save-dev textlint-plugin-mdx{
"plugins": ["mdx"],
"rules": {
"prh": { "rulePaths": ["./prh.yml"] }
}
}プラグインを入れる前は指摘ゼロで終了コード0だったサーバの設定です。を含む.mdxが、入れた後はサーバ => サーバー [Error/prh]と指摘され、終了コード1になりました。
JSXコンポーネントの中身も試しました。<Callout title="注意">で囲んだ本文のユーザと、その外の段落のユーザは、どちらも指摘され、--fixでそろってユーザーに直りました。同じファイルの```bashブロックのechoサーバは、.mdのときと同じく書き換わりませんでした。
プラグインを入れたら、hookのスクリプトのcaseに*.mdxを足します。
case "$file_path" in
*.md|*.mdx|*.txt) ;;
*) exit 0 ;;
esac--stdinと--cacheをhookに使うとどうなるか
CLIには、hookに使えそうなオプションが2つあります。どちらも、このスクリプトの形では得るものが小さいことを、実行して見ました。
--stdinは、ファイルを書き換えない。パイプで本文を渡して検査するオプションで、--stdin-filenameでファイル名を渡す必要があります。ファイル名の拡張子で、どのプラグインを使うかが決まるためです。--fixと組み合わせると、直した結果は--format fixed-resultで標準出力に出るだけです。
printf 'サーバの設定。\n' \
| npx textlint --stdin --stdin-filename a.md --fix --format fixed-result出力はサーバーの設定。で、終了コードは0でした。保存前の本文を検査したいときには向きます。PostToolUseが走る時点で、編集はすでにディスクに書かれています。ファイルのパスをそのまま渡して--fixで直接書き換えるほうが、スクリプトは短く済みます。
--cacheは、1ファイルでは速くならない。 --cacheは変更のあったファイルだけを検査するオプションで、キャッシュの置き場は--cache-locationで変えられます。既定のmetadata戦略はmtimeとサイズで変更を見分けるため、CIでは信頼できないと書かれています。CIで使うなら--cache-strategy contentです。
hookは編集した1ファイルしか渡さないので、効果が出る場面がありません。.mdを1つ検査する時間は、--cacheなしが0.80秒、ありが0.82秒でした。指摘が残っているファイルは、2回目の実行でも同じ指摘が出ました。全文書を一括で検査するCIや保存前のチェックで使うオプションです。
指摘を消す近道を塞ぐ
校正を回すと、Claudeが指摘を直さずに、ルールを外す方向へ流れることがあります。典型は.textlintrc.jsonやprh.ymlの書き換えです。
CLAUDE.mdに方針を書いておくと、判断の指針にはなります。
## 文章の校正
- Markdownの編集後にtextlintのhookが走る。返ってきた指摘は、ルールを緩めずに文を直す
- 表記は`prh.yml`の辞書に従う。辞書にない語を揃えたいときは、追加案を示して確認を取る
- `.textlintrc.json`と`prh.yml`は編集しないただしCLAUDE.mdは文脈で、強制ではありません。確実に止めたい設定ファイルは、権限のdenyルールで編集を拒否します。Edit(/path)のパスは、プロジェクトの設定ファイルに書いた場合、プライマリ作業ディレクトリからの相対位置を指します。
{
"permissions": {
"deny": [
"Edit(/.textlintrc.json)",
"Edit(/prh.yml)"
]
}
}辞書への語の追加を人が決める運用にするなら、この拒否が歯止めになります。権限ルールのパスの書き方はRead・Editルールのパス起点で詳しく扱っています。
よくあるつまずき
- 何も指摘が出ない: 対象が
.mdxなど標準外の形式か、.textlintignoreに載っている可能性があります。どちらも終了コード0で、エラーにはなりません - 存在しないパスで終了コード2になる: textlint v15以降は、存在しないファイルやglobの検索エラーを終了コード2で返します。
TextlintFileSearchError: Not found target files.と出ます。hookでは編集直後のファイルを渡すので通常は起きませんが、スクリプトの手動テストでパスを間違えると出ます .textlintcacheというファイルが増えた:--cacheを付けて実行すると、プロジェクト直下にこのファイルができます。コミット対象に入れたくなければ.gitignoreに足します--fixの書き換えを確認したい:npx textlint --fix --dry-run -f diff <file>で、書き換えずに差分だけを見られます。辞書を作り込む段階では、この形で想定外の置換がないかを確認します
まとめ
textlintをhookに載せる効果は、機械的に直せる表記ゆれと、判断が要る文の組み立てを、別の経路に分けられることです。前者は辞書と--fix、後者は終了コード2でClaudeに返します。
入れる順序は、最小の設定で動かしてから、返ってくる指摘を見て緩める流れが安全です。最初から全ルールを厳しく有効にするより、sentence-lengthの上限やno-mix-dearu-desumasuのように、書き手の文体と合わないものを先に調整すると、hookが静かに動き続けます。