Claude CodeでPhoenixアプリを開発する — mix precommitで完了を判定
Phoenixの新規プロジェクトが持つmix precommitエイリアスを、CLAUDE.mdとStop hookでClaude Codeの完了条件にする手順と、実行時の副作用をまとめます。
Phoenixアプリの完了条件は、新規プロジェクトが最初から持っている mix precommit に任せられます。Claude Codeには「作業を終える前にこれを通す」と伝え、Stop hookで実行を強制すれば、警告つきのコードや未整形のファイルを残したまま止まる場面が減ります。この記事では、そのための設定を順に組み立てます。
Phoenixが最初から用意している完了条件
Phoenixのインストーラーが生成する mix.exs には、precommit というエイリアスが入っています。テンプレートでは次の4つのタスクが並んでいます。
mix precommitの実行順
- 1
compile --warnings-as-errors
コンパイル時の警告をエラーとして扱います。未使用の変数や変更漏れのある関数呼び出しが、この時点で落ちます。
- 2
deps.unlock --unused
どの依存からも使われていない項目を
mix.lockから外します。 - 3
format
mix formatでソースを整形します。 - 4
test
テストスイートを走らせます。
同じ mix.exs には、precommit をtest環境で走らせる指定もあります。cli/0 の preferred_envs: [precommit: :test] です。MIX_ENV=test を手で付けなくても、テスト用の設定でコンパイルとテストが回ります。
Ectoを選んだプロジェクトでは、test エイリアスが ecto.create --quiet と ecto.migrate --quiet を先に実行します。つまり mix precommit の最後の段で、テスト用データベースへの接続が必要になります。
生成されるAGENTS.mdとClaude Codeの関係
Phoenix 1.8のインストーラーは、プロジェクト直下に AGENTS.md を置きます。この中の「Project guidelines」に、mix precommit エイリアスを変更が終わったときに使い、残った問題を直すよう書かれた行があります。つまり、Phoenix側は完了条件をエージェント向けの指示として最初から配っています。
ここでClaude Codeの読み込み規則に注意が必要です。AGENTS.md を直接読むのは、作業ディレクトリかその上位に CLAUDE.md(.claude/CLAUDE.md、CLAUDE.local.md を含む)が無いときだけです。CLAUDE.md を1行でも作ると、Claude Codeは CLAUDE.md 側だけを読み、AGENTS.md は読み込まれなくなります。さらに AGENTS.md の直接読み込みはv2.1.277以降の機能です。
そのため、Phoenixのガイドラインを活かしたままClaude Code向けの指示を足すなら、CLAUDE.md の先頭で @AGENTS.md と取り込む形が素直です。読み込まれない場合の切り分けはAGENTS.mdが動かない2つの原因、二つのファイルを併用する運用はAGENTS.mdとCLAUDE.mdで設定を統合する運用パターンで扱っています。
CLAUDE.mdに書く完了条件
生成されたAGENTS.mdには、LiveViewのフォームや Layouts.app の扱いなど、Phoenix 1.8向けの細かい規則がすでに入っています。そこへ重ねる CLAUDE.md は短くて足ります。
@AGENTS.md
## 完了条件
- 変更を終える前に `mix precommit` を実行し、失敗が残る間は終了しない
- 警告は握りつぶさず、原因のコードを直す(`--warnings-as-errors` を外さない)
- `mix precommit` が書き換えた `mix.lock` と整形差分も、変更の一部として扱う
- 失敗したテストだけを見たいときは `mix test --failed` を使う最後の mix test --failed は、v1.8.0のAGENTS.mdにあるテスト失敗時の手順です。全件を何度も回すより、直前に落ちたものだけを再実行したほうが待ち時間が短くなります。
規約を書いただけでは、Claudeが実行を忘れる余地が残ります。そこで次の節の機械的な確認を足します。
Stop hookでmix precommitを強制する
Stop hookは、Claude Codeが応答を終えようとした瞬間に動きます。decision: "block" を返すか、終了コード2で終わると、Claudeは止まらずに続きの作業へ戻されます。終了コード2のときは、標準エラーの文面が「続ける理由」としてClaudeに渡ります。
.claude/settings.json に次を置きます。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/precommit.sh",
"timeout": 600
}
]
}
]
}
}呼び出されるスクリプトは次のとおりです。
#!/usr/bin/env bash
cd "$CLAUDE_PROJECT_DIR" || exit 0
[ -f mix.exs ] || exit 0
# Elixirのソースや設定に変更が無ければ何もしない
if [ -z "$(git status --porcelain -- lib test config priv mix.exs)" ]; then
exit 0
fi
if out=$(mix precommit 2>&1); then
exit 0
fi
echo "mix precommit が失敗しました。原因を直して再実行してください。" >&2
echo "$out" | tail -n 60 >&2
exit 2スクリプトには実行権限を付けます。
chmod +x .claude/hooks/precommit.shポイントは3つあります。
- 変更が無い会話では走らせません。質問に答えただけのターンで、毎回テストスイートを回すことを避けるためです。
- 出力は末尾60行に絞ります。コンパイルエラーの全文を渡すと、長い出力でコンテキストを消費します。
- 成功時は何も出力せず
exit 0で終えます。
ループが止まらないときの上限
テストが直らない状態でこのhookを置くと、Stopのたびにブロックされ、Claudeが同じ修正を繰り返す可能性があります。Claude Codeには安全弁があり、Stop hookが連続8回ターンを継続させると、次のブロックは無効化されて応答が終わります。この回数はClaudeがツールを呼ぶたびにリセットされます。上限を変えたい場合は環境変数 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP を使い、0にすると上限が無効になります。仕組みの詳細はStop hookの無限ブロックを止める環境変数にまとめています。
hookの入力にある stop_hook_active は、すでにStop hookの結果で続行中であることを示します。「二度目以降は通す」と割り切るなら、スクリプトの先頭でこの値を見て exit 0 する書き方もあります。ただしその場合、1回の修正で直らなかった失敗は検査されずに終わります。このスクリプトは上限8回に任せる方針です。
mix precommitの副作用を知っておく
precommit はチェックだけのコマンドではなく、ファイルを書き換えます。
| ステップ | ファイルへの影響 |
|---|---|
| deps.unlock --unused | ファイルへの影響mix.lock から未使用の項目が消える |
| format | ファイルへの影響.formatter.exs の inputs に合う .ex・.exs・.heex が整形される |
| test | ファイルへの影響設定によりテスト用DBの作成とマイグレーションが走る |
mix format は --check-formatted ではないので、整形差分は失敗にならず、そのまま書き込まれます。Stop hookの後に git diff を見ると、Claudeが触っていないファイルが変わっていることがあります。HTMLを生成するプロジェクトでは、.formatter.exs に Phoenix.LiveView.HTMLFormatter がプラグインとして入り、inputs に *.heex も含まれます。テンプレート側の空白やタグの折り返しも整形の対象です。意図しない差分を避けたいなら、作業の最初に一度 mix format を実行してコミットしておくと、差分が実際の変更だけになります。
最初の依頼文の例
設定が済んだら、最初の依頼で完了条件を一緒に渡すと、指示とhookの両方が同じ方向を向きます。
ログイン済みユーザーだけが見られる /posts 一覧を LiveView で追加して。
実装後に mix precommit を実行し、失敗があれば原因を直して再実行すること。
mix.lock や整形で変わったファイルも、変更内容として最後に報告して。依頼文に書いた内容はCLAUDE.mdと重なりますが、重複には意味があります。CLAUDE.mdは常に読まれる規約、依頼文はその作業に限った指示で、役割が違います。最後の報告を求めておくと、precommit が書き換えたファイルをレビューで見落としにくくなります。
承認プロンプトを減らす許可設定
hookの内部で実行する mix precommit は、Claudeのツール呼び出しではないので権限確認の対象外です。一方、Claudeが作業中に自分で mix precommit を叩く場面は毎回の承認が挟まります。完全一致の許可ルールを置くと、この1コマンドだけ確認なしで通せます。
{
"permissions": {
"allow": [
"Bash(mix precommit)",
"Bash(mix test --failed)"
]
}
}Bash(mix *) のような広い許可は避け、完了条件に使うコマンドだけを列挙します。mix ecto.reset や mix deps.update までワイルドカードに含まれてしまうためです。許可ルールは Bash(npm run build) のように、コマンド全体への完全一致で書けます。
失敗の出方ごとに、Claudeへ何を求めるか
mix precommit の4ステップは、止まる位置によって直し方が変わります。CLAUDE.mdに「どこで落ちたかを最初に報告する」と書いておくと、Claudeが出力の先頭だけ読んで見当違いの修正をするのを減らせます。
- compileで落ちる:
--warnings-as-errorsが警告をエラーにした結果です。未使用の変数やaliasを消す、関数の引数を揃えるなど、警告の原因を直します。警告を出す行をコメントアウトしても解消しないので、原因を特定させます。 - formatで差分が出る: 失敗ではなく書き換えです。整形後のファイルをそのまま採用し、差分を元に戻さないよう伝えておきます。
- testで落ちる: 失敗したテストのファイルと行が出力に含まれます。
mix test test/my_test.exsで単体を回し、直ったらmix precommitを再実行する流れにします。
Stop hookのタイムアウトにも注意が要ります。timeout に達したコマンドhookは取り消され、出力は捨てられるため、ブロックの判断も出ません。テストが重い場合に値を短くすると、失敗を見逃したまま終了する形になります。
他フレームワークとの違い
「検証コマンドを完了条件にする」型は、Claude Codeの開発では共通です。Phoenixの特徴は、そのコマンド自体を生成物が持っている点です。Railsでは bin/rails test や rubocop を自分でCLAUDE.mdに書き、Goでは go vet や go test -race を並べる構成になります。Phoenixでは、まずプロジェクトの mix.exs を見て、precommit の中身を確認すれば足ります。
プロジェクトで独自のチェック(Credoなど)を足したいときも、precommit エイリアスの配列に追加すれば、CLAUDE.mdもhookも書き換えずに完了条件が広がります。
よくあるつまずき
- テストが接続エラーで落ちる: Ecto付きのプロジェクトでは、
mix precommitがテスト用DBに接続します。PostgreSQLなどが起動していないと、コードに問題が無くても失敗します。 - 古いプロジェクトで
mix precommitが無い:precommitが入っているのは新しく生成したプロジェクトのテンプレートです。以前のバージョンで作ったアプリには、aliasesに自分で追加します。 - hookが長く待たされる: 設定の
timeout秒を超えるとhookは打ち切られます。テストが重いプロジェクトでは、値を大きくします。 - チームで共有したい:
.claude/settings.jsonと.claude/hooks/precommit.shをリポジトリにコミットすれば、同じ完了条件が全員のセッションに適用されます。
テスト駆動で小さく進める流れと組み合わせるなら、Claude Codeでテスト駆動開発を回す手順も参考になります。