Claude CodeでMakefileの検証ループを作る — make checkを許可して回す
make testとmake lintをmake checkにまとめ、CLAUDE.mdに1行で書いてpermissionsで許可する手順。ターゲット名の付け方と失敗時の読み方までまとめます。
Claude Codeに検証ループを持たせるなら、Makefileに check ターゲットを1本作り、CLAUDE.mdには make check の1行だけを書く構成が扱いやすくなります。言語やツールが変わっても、Claudeが打つコマンドは変わらないからです。許可ルールも Bash(make check) の1本で済みます。
この記事では、Makefileの最小構成、CLAUDE.mdの書き方、permissionsの絞り方、失敗したときの読み方を順に説明します。
Makefileを検証ループの入口にする理由
Claude Codeは、成否を読み取れるチェックがあると、直して実行して結果を読む反復を自力で回せます。逆にチェックが無ければ、「できたように見える」が唯一の合図になり、人間が検証役を引き受けることになります。チェックの例として、テストスイート・ビルドの終了コード・リンター・出力をフィクスチャと突き合わせるスクリプトが挙げられています。
ここで問題になるのは、プロジェクトごとにチェックのコマンドが違うことです。pytest -q、npm run lint、cargo clippy -- -D warnings のように、Claudeに毎回正しい綴りを覚えさせるのは手間がかかります。
Makefileはこの差を吸収する薄い層になります。
- コマンドの綴りをMakefileに閉じ込められる
- 失敗は終了コードで返る(GNU makeは、エラーがあれば終了コード2で終わる)
- 複数のチェックを1つのターゲットにまとめられる
- 言語を問わず
makeという同じ入口になる
すでにruffとmypyをCLAUDE.mdに固定する方法はClaude Codeでruffとmypyの検証ループを組むで扱っています。本記事はその上位に置く層で、個別のツールではなく入口の設計が主題です。
最小のMakefileを書く
まず、テストとリンターを束ねるMakefileを用意します。下の例は言語に依存しない骨組みで、各レシピの中身は自分のプロジェクトのコマンドに差し替えます。
.PHONY: test lint check
test:
pytest -q
lint:
ruff check .
check: test lint押さえる点は3つあります。
- レシピ行の行頭はタブです。GNU makeのマニュアルは、レシピの各行をタブ文字で始めるよう求めています。スペースで書くと動きません。
.RECIPEPREFIX変数で別の文字に変えることもできますが、既定のタブのままにしておくとClaudeが追記するときも崩れにくくなります .PHONYで擬似ターゲットと宣言します。testやlintはファイル名ではなくコマンドの別名です。宣言が無いと、同名のファイルがあるディレクトリで「最新です」と判断され、レシピが実行されません。.PHONYに挙げたターゲットは暗黙ルールの検索も省略されるので、速度の面でも有利ですcheckの前提条件にtestとlintを並べます。前提条件は左から順に処理されるため、先にテスト、次にリンターの順で走ります
.PHONY に挙げた名前と同名のファイルを置くと、どうなるのか。手元のGNU Make 3.81(macOS標準の /usr/bin/make)で、.PHONY 付きの test ターゲットと、空の test ファイルを同じディレクトリに置いて make test を実行しました。結果は「test: ok」と出力され、レシピは実行されています。宣言を外せば同じ状況でレシピは飛ばされるので、ファイルが生まれやすいプロジェクトほど .PHONY が効きます。
CLAUDE.mdにはmake checkを1行で書く
CLAUDE.mdは短いほど守られやすくなります。長いCLAUDE.mdは規則が埋もれて、Claudeが指示を無視する原因になると説明されています。目安は1ファイル200行未満です。
Makefileが入口になっていれば、検証に関する記述は数行で足ります。次は書き方の一例です。
## 検証
- 変更を終える前に `make check` を実行し、終了コードが0であることを確認する
- 失敗したら出力を読んで原因を直し、`make check` を再実行する
- 個別に確認したいときは `make test` / `make lint` を使う
- `make deploy` は実行しないここで効くのは、「何を打つか」と「いつ打つか」の2点が1行ずつ入っていることです。コマンドの引数や設定値はMakefile側に寄せてあるため、CLAUDE.mdに重複して書く必要がありません。ツールを入れ替えてもCLAUDE.mdは触らずに済みます。
permissionsをmake checkに絞る
検証のたびに許可を求められると、反復が止まります。.claude/settings.json の permissions.allow に登録すれば、確認なしで実行できます。
{
"permissions": {
"allow": [
"Bash(make check)",
"Bash(make test *)",
"Bash(make lint *)"
],
"deny": [
"Bash(make deploy *)"
]
}
}Bash(make *) と書かない理由は、許可の範囲にあります。
許可ルールの書き方による範囲の違い
Bash(make *)
make deploy や make clean を含む、あらゆるターゲットが許可されます。ターゲットに副作用があるMakefileでは範囲が広すぎます。
Bash(make check)
完全一致です。make check -j4 のように引数が付くと一致しません。
Bash(make test *)
make test 単体も、make test -j4 のような引数付きも許可されます。
ルールの書き方には、次の決まりがあります。
- 末尾に半角スペース付きの
*を置くと、引数なしの素のコマンドにも一致します。Bash(make test *)はmake testを含みます - スペースなしの
Bash(make test*)にすると、make testallのような別ターゲットにも一致します &&や;でつないだ複合コマンドは、各部分が別々に判定されます。make check && git pushは、make checkが許可されていてもgit pushの分で確認に回ります
denyはallowより先に評価され、allowで例外を切り出すことはできません。make deploy をdenyに入れておけば、将来 Bash(make *) を足してしまった場合でも止まります。ただしBashのルールはClaudeが書いたコマンド文字列に対する照合です。make -C . deploy のように形を変えた呼び出しは別の文字列になるので、denyは安全境界ではなく、よくある形を止める手段と考えるのが妥当です。本当に実行させたくない処理は、Makefileに載せないか、認証情報をその環境に置かない形で守ります。
projectの .claude/settings.json にあるallowルールは、ワークスペースの信頼ダイアログを承認した後に有効になります。チームで共有するときは、リポジトリにコミットして、各自が初回に承認する流れになります。
ターゲット名で意図を伝える
Claudeはターゲット名とCLAUDE.mdの説明から、どれを打つかを決めます。名前が行為を表していると、説明を足さなくても選択が安定します。
| 名前 | 役割 | 副作用 |
|---|---|---|
check | 役割終える前の総合検証(testとlint) | 副作用なし |
test | 役割テストのみ | 副作用なし |
lint | 役割静的解析のみ | 副作用なし |
fmt | 役割整形(ファイルを書き換える) | 副作用あり |
deploy | 役割公開 | 副作用あり |
分け方の原則は、副作用の有無で名前を分けることです。check は読み取り専用にしておき、整形や自動修正は fmt や fix といった別名のターゲットに出します。check が勝手にファイルを書き換えると、Claudeが変更範囲を把握しにくくなり、差分のレビューも追いにくくなります。
デフォルトゴールにも触れておきます。引数なしの make は、Makefileの最初のターゲットを実行します。.DEFAULT_GOAL := check と書けば、先頭の位置に関係なく既定のターゲットを check に固定できます。ただしCLAUDE.mdには make check と明記しておいたほうが、Claudeが引数なしの make を打つ余地を残さずに済みます。
失敗の読み方 — 終了コードと途中で止まる挙動
実際に check が失敗するときの出力を見ておきます。リンターが常に失敗するMakefileを空のディレクトリに作り、GNU Make 3.81で実行した結果です。
$ make check
test: ok
lint: error found
make: *** [lint] Error 1
$ echo $?
2test が成功し、lint が失敗しています。makeは終了コード2で終わりました。マニュアルでは、makeの終了コードは0(成功)・2(エラー)・1(-q を付けたときに最新でないと判断された場合)の3種類だとされています。
この出力はClaudeにとって次のように読めます。
make: *** [lint] Error 1の[lint]が、失敗したターゲット名を示す- 終了コード2は、1つ以上のレシピが非ゼロで終わったことを意味する
- 失敗したターゲットより後ろは、通常は実行されない
最後の点は設計に影響します。makeは既定では、レシピが非ゼロで終わった時点で処理を打ち切ります。check: test lint の test が落ちれば lint は走りません。1回で全部の失敗を見たいときは -k(--keep-going)を付けます。
$ make -k check
test: ok
lint: error found
make: *** [lint] Error 1
make: Target `check' not remade because of errors.-k を付けると、エラーが出ても他の前提条件を処理し続け、最後に非ゼロで終わります。複数のチェックが独立しているなら、make -k check をCLAUDE.mdに書くと、1回の実行でテストとリンターの両方の失敗を拾えます。その場合は許可ルールも Bash(make -k check) を足します。
実行前の確認には -n(--just-print)が使えます。実行されるレシピを表示するだけで実行しません。新しいターゲットをClaudeに書かせた直後に、レシピの中身を確かめる用途に向いています。ただし、$(MAKE) を含む再帰呼び出しなど、-n でも実行されるレシピがあります。
もう1つ、見落としやすい挙動があります。レシピの各行は別々のシェルで実行されます。1行目の cd は2行目に引き継がれません。ディレクトリを移して処理するなら、cd sub && pytest のように同じ行にまとめるか、make -C sub test と書きます。
つまずきやすい点
タブがスペースに変わる。エディターの設定や、Claudeが書いたMakefileの整形で、行頭のタブがスペースに置き換わることがあります。makeは構文エラーで止まります。.editorconfig にMakefile用の設定を入れておくと防げます。
[Makefile]
indent_style = tabcheck が全部入りで遅くなる。 check に統合テストや重いビルドを入れると、1回ごとの待ち時間が長くなり、Claudeの反復が間延びします。速い検証を check、時間のかかるものを check-full のような別ターゲットに分けると、日常の反復が軽くなります。
出力が多すぎる。成功時に大量のログが出ると、コンテキストを消費します。レシピの先頭に @ を付けると、コマンドの再表示が抑えられます。.SILENT を前提条件なしで指定すれば、全ターゲットでコマンドの表示が止まります。ただし失敗の原因を読む手がかりまで消さないよう、エラー出力は残します。
言語ごとのMakefile以外にも使える。C++ ならCMake presetsとctestを検証コマンドにする方法、モノレポならpnpmの --filterで対象パッケージだけ検証する方法があります。いずれも、make check から呼び出す形にすれば、Claude側の入口は変わりません。
まとめ
Makefileを検証ループの入口にする利点は、Claudeに覚えさせるコマンドを1つに減らせる点です。ツールの更新やプロジェクトの移行があっても、CLAUDE.mdとpermissionsはそのまま使えます。
設計の要点は次の3つです。
checkは副作用を持たせず、失敗を終了コード2で返す- 許可は
Bash(make *)ではなく、必要なターゲット単位で書く - 速い検証と重い検証をターゲットで分ける
まず自分のプロジェクトで、終える前に必ず走らせたいコマンドを書き出し、check にまとめるところから始めてみてください。