Claude Codeでhadolintを使う — Dockerfileの指摘をhookで返して直させる
Claude Codeが書いたDockerfileをhadolintで検査し、指摘をPostToolUse hookでClaudeに返して直させる設定です。DL番号の読み方と、無視コメントで黙らせない運用も扱います。
hadolintは、Dockerfileを構文木に変換してルールを当てるリンターです。RUN命令の中のシェルコードは、ShellCheckに渡して検査します。Claude CodeにDockerfileを書かせたら、編集した直後にhadolintを走らせ、指摘をそのままClaudeへ返す。この記事は、その検査ループをPostToolUse hookで組む手順です。
構成は3つの部品に分かれます。hadolint本体と.hadolint.yaml、hookのスクリプト、そして「指摘は直す。無視コメントで消さない」というCLAUDE.mdの規約です。マルチステージビルドやレイヤー順の最適化はClaude CodeでDockerfileを最適化する手順が扱うので、ここでは検査と修正のループに絞ります。
hadolintの指摘は2種類の番号で届く
hadolintの出力には、DLで始まる番号とSCで始まる番号が混ざります。前者はhadolint自身のルール、後者はShellCheckのルールです。READMEのルール表も、この2系統に分けて載せています。
この違いは、Claudeに直させるときに効いてきます。DL番号はDockerfileの構造(FROMのタグ、USER、COPYとADDの使い分けなど)への指摘で、直す場所はDockerfileの命令そのものです。SC番号はRUNの中のシェルへの指摘なので、直し方はShellCheckのhookで扱ったクォートやエラー処理の話に戻ります。
重症度はerror、warning、info、styleの4段階です。ルールごとの既定値は表で決まっていて、たとえばDL3000(WORKDIRは絶対パス)とDL3020(ADDではなくCOPYを使う)はerror、DL3006(イメージのタグ指定)やDL3008(apt-getのバージョン固定)はwarning、DL3009(aptのリスト削除)やDL3059(連続するRUNの統合)はinfoです。
Claudeがよく踏みやすいDL番号
READMEの表から、Claudeが初稿で書きやすい形に関わるルールを抜き出します。
| 番号 | 既定の重症度 | 指摘の趣旨 | 直し方の例 |
|---|---|---|---|
| DL3006 | 既定の重症度warning | 指摘の趣旨イメージのタグを明示する | 直し方の例FROM node:<version>の形にする |
| DL3007 | 既定の重症度warning | 指摘の趣旨latestは更新で壊れうる | 直し方の例リリースタグで固定する |
| DL3008 | 既定の重症度warning | 指摘の趣旨apt-get installでバージョンを固定する | 直し方の例curl=<version>の形で書く |
| DL3009 | 既定の重症度info | 指摘の趣旨apt-getのリストを削除する | 直し方の例同じRUNの末尾でrm -rf /var/lib/apt/lists/* |
| DL3015 | 既定の重症度info | 指摘の趣旨推奨パッケージを増やさない | 直し方の例--no-install-recommendsを付ける |
| DL3020 | 既定の重症度error | 指摘の趣旨ファイルやフォルダにはADDでなくCOPY | 直し方の例COPYに置き換える |
| DL3002 | 既定の重症度warning | 指摘の趣旨最後のUSERがrootではいけない | 直し方の例末尾で一般ユーザーに切り替える |
| DL3042 | 既定の重症度warning | 指摘の趣旨pip installのキャッシュを残さない | 直し方の例--no-cache-dirを付ける |
| DL4006 | 既定の重症度warning | 指摘の趣旨パイプを含むRUNの前にpipefailを設定する | 直し方の例SHELL ["/bin/bash", "-o", "pipefail", "-c"] |
表の見出し「趣旨」は、READMEの説明文を日本語にしたものです。READMEは自分で「不完全なリスト」と書いているので、手元のhadolintが出す番号は、表にないものも出ます。
指摘にバージョンを固定する系(DL3008、DL3013、DL3018)が並ぶ点は、Claudeとの相性が悪い領域です。固定する値は、Claudeに推測で書かせるより、ビルド環境でapt-cache policyなどを実行させて、出力を読ませてから書かせる運用が確実です。
hadolintを入れて手で動かす
hookに載せる前に、手元で動かします。READMEが挙げるインストール方法は、macOSのHomebrew(brew install hadolint)、Windowsのscoop(scoop install hadolint)、ビルド済みバイナリ、コンテナです。
brew install hadolint
hadolint --version
hadolint Dockerfileローカルに入れたくない環境では、Dockerfileを標準入力に渡す方法があります。
docker run --rm -i hadolint/hadolint < Dockerfile出力の形式は--format(-f)で選べます。選択肢はtty、json、checkstyle、codeclimate、gitlab_codeclimate、gnu、codacy、sonarqube、sarif、junitで、既定はttyです。人間とClaudeが読む用途なら既定のままで足ります。色の制御コードがClaudeへの出力に混ざらないよう、--no-colorは付けておきます。
例えば次のような出力になります(形式を示す説明用の例です)。
Dockerfile:1 DL3006 warning: Always tag the version of an image explicitly
Dockerfile:5 DL3020 error: Use COPY instead of ADD for files and folders
Dockerfile:9 DL3009 info: Delete the apt-get lists after installing something行番号、番号、重症度、メッセージが1行にまとまるので、Claudeは該当の命令を探して直せます。
終了コードを閾値で絞る
hadolintは、ルール違反があると失敗の終了コードで終わります。既定ではinfo以上の違反が対象です。これを変えるのが--failure-threshold(-t)で、error、warning、info、style、ignore、noneから選びます。READMEは、このオプションが影響するのは終了コードだけで、指摘そのものはすべて出力されると明記しています。
hookの設計では、この性質を使います。終了コードをwarningに絞ると、infoの指摘(DL3059など)だけのDockerfileは「通った」と扱われます。ただし出力にはinfoの行も載るので、通ったときの出力は捨てれば静かです。ループを止める基準を重症度で決める、という分担です。
設定はプロジェクト直下の.hadolint.yamlに置けます。
failure-threshold: warning
ignored:
- DL3059
override:
error:
- DL3002
- DL1001overrideは、ルールの重症度を付け替える設定です。READMEの例にもDL3002をerrorに引き上げる書き方があります。DL1001は「インラインの無視pragmaを避けよう」というルールで、既定の重症度はignore(出さない)です。これをerrorかwarningに上げると、Claudeが無視コメントを足した時点で、その行が指摘として返ってきます。この使い方は後の節で説明します。
hadolintが設定ファイルを探す順序は、カレントディレクトリの.hadolint.yaml、$XDG_CONFIG_HOME/hadolint.yaml、$HOME/.config/hadolint.yaml、$HOME/.hadolint/hadolint.yamlなどです。見つけた最初の1つだけを使い、複数を合成しません。リポジトリの直下から実行されるようにしておくと、チーム全員で同じ基準になります。
編集直後に走らせるPostToolUse hook
Claude CodeのPostToolUse hookは、ツールが成功した直後に動きます。matcherにEdit|Writeを指定すれば、ファイルの書き込みごとに起動します。入力のJSONにはtool_input.file_pathが絶対パスで入ります。
.claude/settings.jsonには次のように書きます。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/hadolint.sh",
"timeout": 60
}
]
}
]
}
}スクリプトはDockerfileだけを対象にして、指摘があれば標準エラーに流して終了コード2で終えます。
mkdir -p .claude/hooks
cat > .claude/hooks/hadolint.sh <<'SH'
#!/bin/bash
file_path=$(jq -r '.tool_input.file_path // empty')
[ -z "$file_path" ] && exit 0
case "$(basename "$file_path")" in
Dockerfile|Dockerfile.*|*.Dockerfile|*.dockerfile|Containerfile) ;;
*) exit 0 ;;
esac
[ -f "$file_path" ] || exit 0
cd "${CLAUDE_PROJECT_DIR:-.}" || exit 0
if command -v hadolint >/dev/null; then
output=$(hadolint --no-color --failure-threshold warning "$file_path" 2>&1)
status=$?
elif command -v docker >/dev/null; then
output=$(docker run --rm -i hadolint/hadolint < "$file_path" 2>&1)
status=$?
else
exit 0
fi
if [ "$status" -ne 0 ]; then
echo "$output" >&2
exit 2
fi
exit 0
SH
chmod +x .claude/hooks/hadolint.sh終了コード2を使う理由は、hooksリファレンスにあります。終了コード0で終えたhookの標準エラーは、Claudeに届きません。PostToolUseは、すでに実行されたツールを取り消せない代わりに、終了コード2の標準エラーをClaudeへ見せます。ファイルは書き込み済みなので、返すのは「次に直してほしい指摘」です。
スクリプトのつくりには、押さえておきたい点が3つあります。
- ファイル名で絞る。
Dockerfile.devやapp.Dockerfileも拾い、Containerfileも対象にしています。matcherはEdit|Writeのままです。ifフィールドでも絞れますが、ファイル名のパターンはスクリプトの側で扱うほうが読みやすくなります docker runの分岐は、設定ファイルがコンテナに渡らない。READMEの例のように-vで.hadolint.yamlをマウントしないと、プロジェクトのignoredやoverrideは効きません。そのため、ローカルに入れる方法を第一候補にしています- どちらも無ければ黙って通過する。検査されたのか素通りなのかが曖昧になるので、チームではhadolintの導入手順をREADMEかsetupスクリプトに書いておきます
このhookが拾えないもの
matcherがEdit|Writeのため、Bashコマンドでの書き換えは対象外です。たとえばcat > Dockerfile <<EOFやsed -iで変えた場合、PostToolUseは動きません。hooksリファレンスにも、Edit|Writeに一致するhookはBashコマンドや外部プロセスが同じファイルを書き換えたときには動かない、という記述があります。
Bash経由の変更まで拾いたいときの選択肢は2つです。1つ目はmatcherを*にして、スクリプトの側でgit status --porcelainなどから変更されたDockerfileを探す方法です。2つ目はFileChangedイベントを使う方法です。ただしFileChangedは、監視するファイル名を事前に列挙する必要があり、Dockerfileのようなリテラルの名前には向きますが、Dockerfile.*の広がりには向きません。多くのリポジトリでは、Dockerfileが1つだけなので、FileChangedで足りる場合もあります。
hookの基本構文と他のイベントとの違いはPostToolUse hookでツール実行後の後処理を自動化するに、言語別の検査コマンドの一覧はPostToolUse hookのLint自動修正にあります。Dockerとの付き合い方全般はClaude CodeとDockerを参照してください。
直させるか消させるか — 無視コメントの規律
指摘を返し続けると、Claudeには「最短で緑にする手」が見えてきます。hadolintでは、それが無視コメントです。
# hadolint ignore=DL3006
FROM ubuntuREADMEによると、このコメントは直後の命令1つにだけ効きます。ファイル全体に効かせる# hadolint global ignore=DL3003,DL3006もあります。理由をコメントで残すには、番号の後ろにもう1つ#を置きます。
この仕組みをそのまま放置すると、hookが形骸化します。そこで運用では、次の3つを重ねます。
- CLAUDE.mdに、無視コメントの扱いを書く
DL1001の重症度を上げて、無視コメントの追加自体を指摘にする- 無視コメントを一切認めない場合は、
disable-ignore-pragma: trueを設定する
CLAUDE.mdの規約は、たとえば次のようにします。
## Dockerfileの検査
- Dockerfileを編集したら、hookが返すhadolintの指摘をすべて直す
- `# hadolint ignore=`や`.hadolint.yaml`の`ignored`を足して指摘を消さない
- 直せない理由があるときは、変更せずに理由を報告して判断を待つ
- パッケージのバージョンは推測で書かず、ビルド環境のコマンドで調べてから固定する2つ目のDL1001は、無視コメントを「禁止」ではなく「見える化」する選択です。.hadolint.yamlのoverrideにDL1001を入れると、Claudeが足したignoreの行が、次のhook実行で指摘として返ります。レビューする人間が、無視の追加に気づきやすくなります。
3つ目のdisable-ignore-pragma(コマンドラインでは--disable-ignore-pragma)は、インラインのignoreをすべて無効にします。厳格ですが、人間が書いた正当な例外コメントまで効かなくなる点に注意が要ります。例外が必要になったら、.hadolint.yamlのignoredに理由付きで書くほうが、差分として見えます。
例外を置いてよい場面
すべての指摘が直すべきものとは限りません。社内のレジストリからだけイメージを取る構成では、無視コメントを散らす代わりに、trustedRegistriesの設定で許可するレジストリを宣言できます。READMEの例にはdocker.io、my-company.com:5000、*.gcr.ioのようなワイルドカードの書き方があります。逆に、許可していないレジストリのイメージをFROMに書いたときに警告が出る仕組みなので、Claudeが意図しないイメージを選んだときの検出にも使えます。
ループが回り続けるときの切り分け
hookが終了コード2を返すと、Claudeは指摘を読んで直し、また保存します。そのたびにhookが走ります。ここで同じ指摘が戻り続けるなら、原因は次のどれかです。
- 指摘が直せない種類で、Claudeが別の書き方を試し続けている(CLAUDE.mdに「直せないときは報告する」と書く対策が効きます)
failure-thresholdが低すぎて、infoの指摘でも毎回止まっている- hookの
timeoutが短く、hadolintが時間内に終わっていない
終了コード2でなく0で素通りしているときは、hookが動いていません。/hooksメニューで設定が読み込まれているか、jqが入っているか、スクリプトに実行権限があるかを確かめます。
全体の流れ
Dockerfileの検査ループ
- 1
Claudeが編集する
EditまたはWriteで、Dockerfileを書き換えます。
- 2
hookが起動する
ファイル名がDockerfile系のときだけ、hadolintを実行します。
- 3
指摘を返す
警告以上があれば、標準エラーに出力し、終了コード2で終えます。
- 4
Claudeが直す
CLAUDE.mdの規約に従い、無視コメントを足さずに命令を直します。
この流れでは、Dockerfileを人間が読む前に、機械で拾える定型の指摘が片付きます。レビューでは、最適化やセキュリティ設計といった判断の要る点に時間を使えます。
よくある質問
hadolintはDockerfileのビルドも検証してくれますか
hadolintは、Dockerfileを構文木として読んでルールを当てる静的な検査です。イメージをビルドしないので、ビルドが通るかどうかは別に確かめます。ビルド可否まで見たいときは、docker buildを実行させる検査を別のhookかStop時の確認に足します。
Claudeにhadolintを手で実行させるだけでは足りませんか
足りる場面もあります。小さな修正なら、CLAUDE.mdに「Dockerfileを変えたらhadolintを実行する」と書く方法で済みます。ただし、書いた規約をClaudeが毎回守るとは限りません。hookは編集のたびに必ず走るため、「忘れた」が起きません。