PostToolUse hookのLint自動修正 — 言語別コマンド12種の早見表
PostToolUse hookでLintとFormatを自動修正するコマンドを、ESLintからSwiftLintまで12種の早見表でまとめます。修正できない指摘が残ったときの伝え方も扱います。
PostToolUse hookでLintを自動修正する仕組み
Claude CodeがファイルをEdit・Writeしたあとに、ESLintやPrettierなどのLint/Formatツールを自動で走らせて直す。これがPostToolUse hookでの自動修正の基本形です。matcherとhandlerの基本構文はPostToolUse hookでツール実行後の後処理を自動化するに譲り、本記事ではLint/Formatに特化した言語別のコマンドと、直せない指摘が残ったときの扱いに絞って扱います。Hooks全体の33種類のイベント一覧はClaude Code Hooks完全ガイドにまとめています。
PostToolUse hookには1つ制約があります。ツールの実行結果をブロックできません。ファイルの書き込みはすでに完了しているため、フックにできるのは後始末とClaudeへのフィードバックだけです。この制約は、Lint自動修正フックの設計に直接影響します。フォーマットのように必ず成功する処理は問題になりませんが、Lintのように「直せる指摘」と「直せない指摘」が混在するツールでは、直せなかった分をどうClaudeに伝えるかが本題になります。
設定の最小形 — 対象拡張子はifかスクリプト側で絞る
最小構成は、Edit|Writeにマッチさせたハンドラーに自動修正コマンドを1本書くだけです。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/lint-autofix.sh",
"args": []
}
]
}
]
}
}言語ごとにコマンドを切り替えたい場合、ハンドラーのifフィールドで拡張子ごとに分ける方法と、1本のスクリプトの中でtool_input.file_pathの拡張子を見て分岐する方法の2つがあります。if: "Edit(*.py)"のように書けば、Pythonファイルのときだけ動くハンドラーを別に定義できます。ツールが多い場合はスクリプト側で分岐したほうが設定は短くなります。後半の実装例はこの方式で書いています。
言語別Lint/Format自動修正コマンド早見表
自動修正コマンドの効き方はツールによって性格が違います。フォーマッタは基本的に全件を直しますが、Lintツールは「安全に直せる指摘」だけを直し、残りは検出のみで終わります。型検査ツールにはそもそも自動修正コマンドがありません。
| 対象 | ツール | 自動修正コマンド | 自動修正の範囲 |
|---|---|---|---|
| JS/TS(コード品質) | ツールESLint | 自動修正コマンドeslint --fix <file> | 自動修正の範囲ルール違反のうち安全な修正のみ。--fix-typeで対象を絞れる |
| JS/TS/CSS/JSON等(整形) | ツールPrettier | 自動修正コマンドprettier --write <file> | 自動修正の範囲整形のみ。コード品質のルールは見ない |
| JS/TS/CSS/JSON等(一括) | ツールBiome | 自動修正コマンドbiome check --write <file> | 自動修正の範囲Lint・Format・import整理を1コマンドで適用 |
| CSS/SCSS | ツールstylelint | 自動修正コマンドstylelint --fix <file> | 自動修正の範囲修正可能なCSSルール違反のみ |
| Python(Lint/Format) | ツールruff | 自動修正コマンドruff check --fix <file> / ruff format <file> | 自動修正の範囲checkは既定で安全な修正のみ(unsafeな修正は--unsafe-fixesが別途必要)。formatは整形専用 |
| Python(型検査) | ツールmypy | 自動修正コマンドmypy <file> | 自動修正の範囲自動修正コマンドは無い。型エラーの報告のみ |
| Python(型検査) | ツールpyright | 自動修正コマンドpyright <file> | 自動修正の範囲自動修正コマンドは無い。型エラーの報告のみ |
| Go | ツールgolangci-lint | 自動修正コマンドgolangci-lint run --fix | 自動修正の範囲リンター・フォーマッターが検出した指摘のみ。対応の有無はリンターごとに異なる |
| Rust | ツールClippy | 自動修正コマンドcargo clippy --fix | 自動修正の範囲一部のlintのみ自動修正。未コミットの変更が残っているとfixが拒否される場合がある |
| Kotlin(整形) | ツールktlint | 自動修正コマンドktlint -F | 自動修正の範囲カレントディレクトリ以下の.kt/.ktsを一括走査。単一ファイルはパスを引数で渡す |
| Kotlin(静的解析) | ツールdetekt | 自動修正コマンドdetekt --auto-correct | 自動修正の範囲既定のルールセットは自動修正非対応。--pluginsでktlintのルールセットを追加しないと何も直らない |
| Swift | ツールSwiftLint | 自動修正コマンドswiftlint --fix | 自動修正の範囲修正可能なルール違反のみ。--strictと組み合わせる例が多い |
表からわかるとおり、ツールの性格は大きく3種類に分かれます。フォーマッタ(Prettier・ktlint・SwiftLintの整形部分)は入力を1つに正規化するだけなので、失敗という状態そのものがほぼ発生しません。Lintツール(ESLint・stylelint・ruffのcheck・golangci-lint・Clippy)は安全な指摘だけを直し、判断が要る指摘を残します。型検査ツール(mypy・pyright)は書き換え機能を持たず、検出だけで終わります。同じ--fixという見た目のオプションでも、フックの後段で「直った」「直らなかった」を判定するロジックが必要になるのは、後の2種類だけです。
BiomeのようにLintとFormatを1コマンドにまとめたツールを使うか、ESLintとPrettierのように役割を分けたツールを組み合わせるかは、プロジェクトの既存構成に依存します。両方を同じディレクトリに残したまま二重に走らせると、ESLintの一部ルールとBiomeのLintルールが同じ違反を別の言い回しで報告し、Claudeが読む指摘が重複します。フックを新設するタイミングで、どちらか一方の設定ファイルを無効化しておくと、この重複を避けられます。
自動修正後も直らない指摘をどうClaudeに伝えるか
Lintツールの--fixは魔法ではありません。ESLintのno-unused-varsのように、変数を消してよいかどうか判断が要るルールは自動修正の対象から外れます。detektのように、--auto-correctを渡しても既定のルールセットでは何も直らないツールもあります。フックの設計で本当に大事なのは、この「直らなかった分」をClaudeにどう見せるかです。
PostToolUseはツールの実行結果を差し戻せませんが、終了コード2を返すとClaudeに標準エラー出力(stderr)が見える形で表示されます。これを使い、Lintの再チェック結果が残っている場合だけ終了コード2にする設計が基本形になります。
| 状態 | 終了コード | Claudeに伝わる内容 |
|---|---|---|
| 自動修正で全て解消 | 終了コード0 | Claudeに伝わる内容デバッグログのみ。会話には何も表示されない |
| 自動修正後も指摘が残る | 終了コード2 | Claudeに伝わる内容stderrの内容がツール結果の横に表示される(実行済みのツールをブロックすることはできない) |
| 型検査ツールでエラーを検出 | 終了コード2 | Claudeに伝わる内容同上。修正コマンドが無いツールでは、検出したらそのまま終了コード2にする設計になる |
| リンター自体が異常終了 | 終了コード2以外の非0 | Claudeに伝わる内容JSONを出していない限り非ブロッキングのエラー扱いとなり、<hook name> hook errorという通知だけが表示される。エラーの詳細は伝わらないことがある |
この仕組みが繰り返されると、Claudeが次のターンでstderrの指摘を読み、該当箇所を編集し、フックがまた自動修正とチェックを走らせる、という運用上のループになります。ただしこれはClaude Codeが保証する自動リトライではなく、Claudeがstderrを読んで自発的に動くかどうかに依存します。フックが返せるのは「直せなかった事実をClaudeの目の前に置く」ところまでです。
終了コード2を使わず、JSON出力のdecision: "block"とreasonで伝える設計もあります。PostToolUseはdecisionとreasonを最上位のフィールドとして扱う側のイベントなので、標準出力に{"decision": "block", "reason": "..."}を出し、終了コードは0のままにする形です。この場合、フックスクリプト自体は正常終了として扱われつつ、指摘の文面だけがツール結果の横に添えられます。フックの実行が失敗したのかLintの指摘が残っているだけなのかを区別してログに残したいときは、stderr+終了コード2よりもこちらのほうが扱いやすくなります。
複数言語をディスパッチする自動修正フックの実装例
拡張子で分岐し、自動修正後に再チェックして残りがあれば終了コード2にするスクリプトの例です。
mkdir -p .claude/hooks
cat > .claude/hooks/lint-autofix.sh <<'SH'
#!/bin/bash
input=$(cat)
file_path=$(jq -r '.tool_input.file_path // empty' <<<"$input")
[ -z "$file_path" ] && exit 0
case "$file_path" in
*.ts|*.tsx|*.js|*.jsx)
npx eslint --fix "$file_path" >/dev/null 2>&1
remaining=$(npx eslint "$file_path" 2>&1)
;;
*.py)
ruff check --fix "$file_path" >/dev/null 2>&1
remaining=$(ruff check "$file_path" 2>&1)
;;
*.go)
golangci-lint run --fix "$file_path" >/dev/null 2>&1
remaining=$(golangci-lint run "$file_path" 2>&1)
;;
*)
exit 0
;;
esac
if [ -n "$remaining" ]; then
echo "$remaining" >&2
exit 2
fi
exit 0
SH
chmod +x .claude/hooks/lint-autofix.sh拡張子ごとに「自動修正コマンド」と「再チェックコマンド」の2回コマンドを呼んでいるのは、--fixのコマンド自体は直せた・直せなかったの情報を必ずしもわかりやすい形で返さないためです。ESLintやruffは--fix後にまだ違反が残っていれば非0で終了しますが、その終了コードだけを見てexit 2に直結させると、直せなかった内容がstderrに残らない場合があります。再チェックを別に走らせて出力をremainingに集め、それが空でなければechoしてからexit 2にすることで、Claudeに見える内容を自分で組み立てられます。
実行時間を抑える工夫 — 単一ファイルの範囲に留める
上のスクリプトはtool_input.file_pathで渡された1ファイルだけを対象にしています。eslint --fix .のようにプロジェクト全体を毎回スキャンする書き方もできますが、Editのたびにプロジェクト全体のLintが走ると、コマンドhookの既定タイムアウトである600秒に近づく規模のプロジェクトも出てきます。単一ファイルに絞れば、実行時間はほぼファイル1つの解析コストだけで収まります。
golangci-lintやClippyのようにビルドを伴うツールでは、単一ファイル指定でもコンパイル自体の時間がかかります。どうしても重くなる場合は、ハンドラーにasync: trueを付けてバックグラウンド実行にする選択肢があります。ただし通常のasyncはClaudeへの応答を待たずに完了するため、直せなかった指摘をその場でClaudeに見せることはできません。失敗したときだけClaudeを起こしたいなら、asyncRewake: trueを使います。バックグラウンドの終了コードが2のときにstderr(空ならstdout)がシステムリマインダーとしてClaudeに渡るため、重いチェックを裏で走らせつつ、問題があったときだけ通知する構成にできます。
よくあるつまずき
detektに--auto-correctを渡しても何も直らない
detektの既定のルールセットは自動修正に対応していません。フォーマット関連の修正を効かせたい場合は、--pluginsでktlintのルールセットを追加する必要があります。追加なしで--auto-correctだけ渡すフックは、指摘は出るが1文字も直らない状態になります。
.cmd・.batのシムをexec形式で直接呼んで失敗する
Windowsでnode_modules/.binにインストールされるESLintやPrettierの.cmdシムは実行ファイルではないため、argsを指定したexec形式では起動できません。commandをnodeにして本体スクリプトのパスをargsに渡すか、argsを省いたシェル形式で呼び出します。この制約自体はPostToolUse hookの記事で扱っている一般的な落とし穴です。
golangci-lintの--fixが一部の指摘を直さない
--fixは「リンターとフォーマッターが検出した修正のうち、そのリンターが対応しているものだけ」を適用します。対応していないリンターの指摘は、--fixを付けても残り続けます。整形だけを目的にするならgolangci-lint fmtという専用サブコマンドもあり、run単体はチェックのみでファイルを書き換えません。再チェックで検出した残りをそのままClaudeに渡す設計であれば、この差は特に問題になりません。
Clippyの--fixが未コミットの変更で拒否される
Clippyの--fixはcargo fixの仕組みを使って動くため、作業ディレクトリに未コミットの変更が残っていると拒否されることがあります。Claude Codeの編集は基本的にコミット前の状態で走るので、この状況に当たりやすくなります。--allow-dirtyを明示するかどうかは、フックの中で変更を巻き込んでよいかの判断になります。
mypy・pyrightに--fix相当のフラグを探してしまう
型検査ツールは指摘を報告するだけで、コードを書き換える機能を持ちません。同じフックの中でESLintやruffと並べて「自動修正」を期待すると、想定と違う挙動に見えます。型検査の結果は、検出した時点でそのまま終了コード2にしてClaudeに渡す設計になります。
まとめ
PostToolUse hookでのLint自動修正は、--fix系のコマンドを1本足すだけでは完結しません。フォーマッタは全件を直しますが、LintツールはESLintのruleごとの安全性判定やruffのsafe/unsafeの区分がある分、直せる範囲が限られます。detektのように追加設定なしでは何も直らないツールもあります。フックの実装で本当に必要なのは、自動修正コマンドの選定よりも、直せなかった指摘を再チェックで拾い、終了コード2でClaudeの目に見える形にする後半部分です。matcherやifの基本設定はPostToolUse hookでツール実行後の後処理を自動化する、失敗時だけ動く別系統のフックはPostToolUseFailure hookでツール失敗時だけ動くリカバリを書くを参照してください。