Claude CodeでValeを回す — 文体と用語の指摘をhookで返して直させる
Valeの.vale.iniとstylesを最小構成で作り、PostToolUse hookでMarkdownの保存のたびに走らせて、文体や用語の揺れをClaude Code自身に直させる手順です。
Claude CodeにREADMEや技術文書を書かせると、「Node.js」が「nodejs」になったり、「simply」「arguably」のような曖昧な言い回しが混ざったりします。こうした文体と用語の揺れを、Valeで機械的に検出して、指摘をそのままClaude Codeに返して直させるのがこの記事の内容です。
Valeの設定ファイル2つと、PostToolUse hookのスクリプト1本で組めます。ポイントはValeの終了コードとhookの終了コードの対応で、ここを取り違えると指摘がClaudeに届きません。
Valeは文法ではなく文体の一貫性を見る道具
Valeは、文章にコードのようなlintをかけるコマンドラインツールです。Vale自身の説明では、文法の正しさよりも文体を見るツールで、複数の書き手の文章を共通のガイドラインでそろえる用途を想定しています。Grammarlyのような文章の添削支援とは位置づけが違います。
もう一つの特徴は、Valeが独自の助言を持たないことです。Valeは規則を書くための枠組みで、何を指摘するかはYAMLで書いた規則(スタイル)が決めます。ブロックコードとインラインコードは既定で検査対象から外れるので、READMEのコマンド例が誤検知になりにくい作りです。
スタイルの検査と書式の検査は別物です。見出しの階層や空行のような体裁はmarkdownlint-cli2をhookで回す記事の担当で、日本語の表記ゆれはtextlintをhookで回す記事の担当です。Valeに任せるのは、英語の用語統一と言い回しの規約になります。
.vale.iniとstylesの最小構成
まずValeを入れます。システム全体に入れる方法のほか、プロジェクトに固定する方法があります。
brew install vale # macOS
npm install --save-dev @vvago/vale # プロジェクトに版を固定する場合
vale --versionシステム全体のインストールだと、コントリビューターやCIごとに版がずれます。新しい版が規則を足したり、既存の規則の一致範囲を変えたりするため、チームで使うならnpmやpipなどで版を固定する方法が用意されています。
設定は、プロジェクトのルートに置く.vale.iniと、styles/ディレクトリの2つです。
.vale.ini
styles/
├── House/
│ ├── Terms.yml
│ └── Hedging.yml
└── config/
└── vocabularies/
└── Project/
├── accept.txt
└── reject.txt.vale.iniは次のとおりです。
StylesPath = styles
MinAlertLevel = suggestion
Vocab = Project
[formats]
mdx = md
[*.{md,mdx}]
BasedOnStyles = Vale, House
Vale.Terms = error
Vale.Avoid = error
Vale.Spelling = warning各行の役割は次のとおりです。
StylesPathは、スタイルとconfig/の置き場所です。.vale.iniからの相対パスで、ディレクトリが存在している必要がありますVocabは、styles/config/vocabularies/にある語彙の名前です。次の節で中身を作ります[formats]のmdx = mdは、.mdxをMarkdownとして読ませる指定です。セクション名はディスク上のファイル名で照合されるので、[*.{md,mdx}]のように.mdxも含めて書きますBasedOnStylesにValeを入れるのは、Vale.TermsやVale.Avoid、Vale.Spellingが内蔵スタイルのValeに属しているためですVale.Terms = errorのようにStyle.Rule = レベルと書くと、規則ごとに重大度を変えられます
重大度は後で効いてきます。errorの指摘が1件でもあるとValeは終了コード1を返し、warningとsuggestionは終了コードを変えません。どの指摘をClaudeに必ず直させるかを、このlevelで決めるわけです。
独自のスタイルHouseには規則を2つ置きます。1つ目は、表記の揺れを正しい表記に置き換えるsubstitutionです。
# styles/House/Terms.yml
extends: substitution
message: Use '%s' instead of '%s'.
level: error
ignorecase: true
swap:
'node[.]?js': Node.js
'java[ -]?scripts?': JavaScript
'claude code': Claude Code左辺に正規表現で揺れのパターンを書き、右辺に正しい表記を書きます。大文字小文字を無視して一致させたうえで、右辺と完全に一致しない出現だけが指摘されます。Nodejsやnode.jsは引っかかり、Node.jsは通ります。この例はValeのドキュメントにあるLinodeの用語ルールに沿った形です。
2つ目は、曖昧な言い回しを検出するexistenceです。
# styles/House/Hedging.yml
extends: existence
message: Consider removing '%s'
level: warning
ignorecase: true
tokens:
- appears to be
- arguably
- simplyこちらはwarningにしています。言い回しの好みは機械的に直すと文意が変わりかねないので、errorにはしません。
vocabで固有名詞の誤検知を減らす
製品名やコマンド名は、Valeから見ると知らない単語です。Vale.Spellingが有効だと、PostToolUseのような辞書にない語が指摘の対象になります。語彙(vocabulary)に登録すると、これを減らせます。
styles/config/vocabularies/Project/accept.txtに、1行1エントリで書きます。
Claude Code
CLAUDE.md
PostToolUse
Opus
pnpm語彙の効果は3つあります。
accept.txtの語は、BasedOnStylesにあるすべてのスタイルの例外リストに加わります。サードパーティ製のスタイルを使っても、プロジェクト側は語彙だけ直せば済みますaccept.txtの語はVale.Termsにも渡され、大文字小文字を含めて完全に一致する書き方だけが許されますreject.txtの語はVale.Avoidに渡され、出現するたびに指摘されます
エントリは正規表現で、大文字小文字を区別します。MongoDBと書けば「mongoDB」は指摘されます。揺れを許したい語は(?i)MongoDBのように書きます。ピリオドは、他の正規表現の構文を使わない限りただの文字として扱われるので、CLAUDE.mdはそのまま書けます。#とスペースで始まる行はコメントです。
使ってほしくない語はreject.txtに置きます。
[Uu]tilize
[Ll]everageここで気をつけたいのは、accept.txtに入れた語をreject.txtに重ねて書く必要がない点です。JavaScriptをaccept.txtに入れれば、Vale.Termsが正しい大文字小文字を強制します。
手で一度回して出力の形を確かめる
hookに載せる前に、ターミナルで1回回します。--output=lineは1件1行で出力する形式で、スクリプトで扱いやすい形です。
vale --output=line README.md
echo $?
vale ls-config出力はファイル:行:列:規則:メッセージの並びです。たとえば次のような行になります(形式はValeのドキュメントのサンプルに沿ったものです)。
README.md:3:17:House.Terms:Use 'Node.js' instead of 'nodejs'.2行目の$?は、errorが1件でもあれば1、なければ0です。設定ファイルが見つからない、規則の読み込みに失敗した、といったVale自体が動けない場合は2になります。vale ls-configは、その実行で使われる設定を、参照元まで解決して表示します。指摘が出ない、期待と違う規則が動く、というときの切り分けに使えます。
PostToolUse hookで保存のたびにValeを走らせる
PostToolUseはツール呼び出しが成功した直後に動くイベントです。matcherにEdit|Writeを指定すると、ファイルを書き換えるツールの後だけ動きます。フックの標準入力にはJSONが渡り、編集したファイルのパスはtool_input.file_pathから取れます。
スクリプトは.claude/hooks/vale-check.shに置きます。
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
case "$FILE" in
*.md|*.mdx) ;;
*) exit 0 ;;
esac
command -v vale >/dev/null 2>&1 || exit 0
ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
cd "$ROOT" || exit 0
OUT=$(vale --output=line "${FILE#"$ROOT"/}" 2>&1)
STATUS=$?
if [ "$STATUS" -eq 1 ]; then
echo "$OUT" >&2 # stderr が Claude へのフィードバックになる
exit 2
elif [ "$STATUS" -ne 0 ]; then
echo "vale を実行できませんでした (exit $STATUS): $OUT" >&2
exit 1
fi
exit 0パスを相対に直しているのは、.vale.iniのセクション([*.{md,mdx}]など)がValeに渡したパスのままで照合されるからです。プロジェクトルートにcdしてから、ルートからの相対パスで渡せば、docs/api/*.mdのようなセクションも想定どおりに効きます。
次に.claude/settings.jsonで登録します。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/vale-check.sh"
}
]
}
]
}
}スクリプトに実行権限を付け、サンプルのJSONを流して動作を確かめます。
chmod +x .claude/hooks/vale-check.sh
echo '{"tool_input":{"file_path":"'"$PWD"'/README.md"}}' \
| .claude/hooks/vale-check.sh
echo $?README.mdにnodejsと書いてあれば終了コードは2になり、指摘が標準エラーに出ます。/hooksで登録内容も見られます。ClaudeにREADME.mdへ「nodejsで動かします」という1文を足させて、直後に指摘が返って修正されれば完成です。
Valeの終了コードとhookの終了コードは意味が違う
このスクリプトの肝は、Valeの1をhookの2に変換している箇所です。
| Valeの終了コード | Valeでの意味 | hookに返す終了コード | hookでの結果 |
|---|---|---|---|
| 0 | Valeでの意味errorが0件 | hookに返す終了コード0 | hookでの結果成功。何も起きない |
| 1 | Valeでの意味errorが1件以上 | hookに返す終了コード2 | hookでの結果stderrがClaudeに渡される |
| 2 | Valeでの意味Vale自体が動けない | hookに返す終了コード1 | hookでの結果非ブロッキングエラー。処理は続行する |
Valeの1をそのまま返すと、hookとしては「0と2以外の終了コード」になり、非ブロッキングエラーとして処理が続行されます。Claudeに指摘を見せたいなら2で終える必要があり、そのときの標準エラーがClaudeへのフィードバックになります。ツールはすでに実行済みなので取り消しはできず、「直してほしい」という情報を返すだけです。
Valeの2は別の話です。設定の不備のようにValeが動けない状態なので、これをhookの2に転送すると、Claudeが.vale.iniを書き換えて直そうとしかねません。スクリプトでは別の分岐に分け、hookの1で人間にだけ知らせています。
もう一つの帰結が、warningとsuggestionはClaudeに届かない点です。House.Hedgingのようなwarningは、エディタやターミナルでは見えても、hookでは終了コードを変えません。Claudeに必ず直させたい規則だけをerrorにする、という運用になります。
Claudeに指摘を直させるときの約束事
指摘が返ってきたとき、Claudeが最短で緑にする方法は、規則を無効にしたり、語彙に追加したりすることです。CLAUDE.mdに約束事を書いておくと、この近道を抑えられます。
## 文書のlint
- .mdの編集後にValeの指摘がstderrで返ってきたら、本文を直して解消する
- .vale.iniとstyles/配下は、指摘を消すために編集しない
- 製品名・固有名詞の誤検知だと考えたら、accept.txtへの追記案を提示し、了承を得てから追加する設定ファイルへの編集そのものを止めたい場合は、PreToolUseのhookで特定のパスへの編集をブロックする方法もあります。hooksガイドの「Block edits to protected files」に、保護対象のパターンに一致するパスへの編集をexit 2で拒否する例があります。
運用の目安は、誤検知か本物の指摘かを人間が決める、という分担です。語彙への追加は、新しい製品名が文書に入ったタイミングでまとめて行えば、accept.txtが少しずつ育ちます。
つまずきやすい点
落とし穴
valeコマンドが無いと、hookは何も言わずに通る
スクリプトはcommand -v valeで未インストールを検出すると、そのままexit 0で終わります。Valeが入っていない環境でもClaudeの作業は止まりませんが、lintが働いていることにもなりません。新しい環境では、手でvale --versionを実行してから使い始めるのが確実です。
StylesPathを丸ごと.gitignoreに入れると、自作のHouseスタイルとconfig/vocabularies/も対象になります。Valeのクイックスタートは「ダウンロードしたスタイルのためにStylesPathを.gitignoreへ」と説明していますが、Packagesで入れたスタイルのフォルダだけを除外する書き方なら、自作の規則はコミットできます- 設定ファイルに書いた
Packagesはvale syncで入ります。Microsoftのようなパッケージを使う場合は、同期してからBasedOnStylesに加えます Vale.Spellingは辞書にない語を指摘するので、技術語の多い文書では件数が増えます。語彙に積む前に、Vale.Spelling = NOで止めてVale.Termsだけを使う構成も選べます- hookの対象は
EditとWriteだけです。sedなどBashコマンドでの書き換えには反応しません。ファイルの変更を契機にしたい場合は、hooksガイドがFileChangedイベントを案内しています
まとめ
Valeは、errorの指摘が終了コード1になる仕組みを持つので、hookに載せるときはその1を2に変換する一手間が要ります。変換さえ入れれば、PostToolUseで保存のたびに用語統一をかけ、Claudeが自分で書いた文書を自分で直す流れが作れます。
最初はHouse.Termsとreject.txtの数行だけをerrorにして、数日運用してみるのが現実的です。誤検知はaccept.txtに積み、効いてきたらwarningの規則をerrorに上げます。英語の文体はValeに、書式はmarkdownlint-cli2に、日本語はtextlintに分けて載せると、それぞれの指摘が混ざらずに読めます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。