Claude CodeでNxのaffectedをフックから使う設計
Nxのaffectedコマンドで変更影響範囲だけを検証する設計を、Claude CodeのStop・PostToolUse・FileChangedフックの使い分けとともに解説します。
Nxのaffectedが解決する問題
Nxのaffectedは、Gitの差分とNxのプロジェクトグラフを突き合わせ、変更の影響を受けるプロジェクトだけを特定するコマンドです。モノレポでプロジェクト数が増えると、コミットのたびに全プロジェクトをビルド・テストするのは現実的でなくなります。
nx affected -t <task> を実行すると、Nxはまず変更されたファイルを特定し、そのファイルが属するプロジェクトを割り出します。次にそのプロジェクトに依存する他のプロジェクトまで辿り、該当する範囲だけでタスクを走らせます。影響範囲は nx graph --affected で可視化できます。
nx affected -t test
nx graph --affectedClaude Codeでコード編集を任せる場面でも、この考え方はそのまま生きます。エージェントが1つのファイルを直しただけなのに、CIと同じ全件テストをローカルでも毎回走らせていては、セッションの待ち時間が長くなります。変更が及ぶ範囲だけを、セッションの区切りで自動検証するのがこの記事の設計です。
ローカルで使うときの注意が1つあります。affectedの既定の基準(base)はmainブランチで、比較先(head)は現在のファイルシステムです。affectedはmainからの差分で計算されるため、変更を重ねても選ばれるプロジェクト群は変わりません。Nxの公式ドキュメントも、10プロジェクトが影響を受けた状態で変更を重ねれば、毎回その10プロジェクトでタスクを走らせることになると説明しています。絞り込みだけでは再実行を避けきれず、リモートキャッシュや分散実行との併用が勧められているのはこのためです。
モノレポの権限設計自体はClaude Codeモノレポ設計で扱っているので、CLAUDE.md階層と合わせて読むと構成の全体像がつかめます。
Claude CodeのどのフックでNx affectedを実行するか
Claude Codeのフックは、セッションのライフサイクル上の特定のタイミングでスクリプトやHTTPエンドポイントを呼び出す仕組みです。nx affectedをどこで実行するかは、PostToolUse と Stop のどちらを選ぶかでほぼ決まります。
PostToolUse はツール呼び出しが成功した直後に発火します。matcher を Edit|Write に絞れば、Claudeがファイルを書き換えるたびにスクリプトが走ります。1回の編集ごとに発火するため、編集の数だけ同じ検証が走ります。
もう1つ見落としやすい点があります。Edit|Write に絞った PostToolUse は、Bash コマンドや外部プロセスが同じファイルを書き換えても発火しません。Claudeが sed や生成コマンドでファイルを変えた回は、検証が素通りします。Bash にマッチさせた PostToolUse なら、tool_response.bashEditDiff で変更されたファイルを受け取れます(v2.1.269以降が必要です)。この記録は、autoモードと bypassPermissions モードで、Claude CodeがBash経由の編集を指示したときに残ります。通常の権限モードでは bashEditDiffEnabled 設定で有効にします。
Stop はClaudeがそのターンの応答を終えたタイミングで発火し、編集の手段を問いません。何ファイル編集しても、応答が完了する時点でaffectedスコープを検証できます。decision: "block" を返せばClaudeの終了そのものを止められるので、テストが失敗したまま応答を終わらせず、Claudeに差し戻せます。
FileChanged はファイル監視専用のフックで、こちらは Bash 経由の書き換えも拾います。ただし用途には向きません。理由は2つあります。
matcherは|区切りのファイル名リストとして登録され、^\.envのような正規表現は、その文字列どおりの名前のファイルを監視してしまう- 判定を返す仕組みがなく、書き込みもClaudeの終了も止められない
モノレポ全体のソース変更を拾うにも、失敗を差し戻すにも合いません。
フックの設定・イベント・decision制御の全体像はClaude Code Hooks完全ガイドにまとめてあります。
Stopフックでaffectedスコープを検証する設定
.claude/settings.json に Stop フックを1つ登録します。Stop はmatcher非対応のイベントで、毎回発火するため、matcher フィールドは書きません。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-nx-affected.sh",
"timeout": 120
}
]
}
]
}
}スクリプトは標準入力のJSONを読み、次の順で動きます。
Stopフックが1回発火したときの流れ
- 1
入力JSONから cwd と stop_hook_active を取り出す
Claudeが応答を終えると
Stopが発火し、スクリプトにJSONが渡されます。 - 2
stop_hook_active が true ならそのまま終了
差し戻しの結果として続行中のターンでは、再検証せず
exit 0で抜けます。 - 3
cwd に移動して nx affected を実行
lintとtestを、mainとの差分に絞って走らせます。 - 4
失敗したときだけ decision: block を標準出力へ
出力末尾20行を
reasonに入れて返すと、Claudeはその内容を受け取って修正に戻ります。成功なら何も出さずに終えます。
#!/bin/bash
input=$(cat)
cwd=$(jq -r '.cwd' <<<"$input")
active=$(jq -r '.stop_hook_active' <<<"$input")
if [[ "$active" == "true" ]]; then
exit 0
fi
cd "$cwd" || exit 0
output=$(npx nx affected -t lint test --base=main 2>&1)
status=$?
if [[ $status -ne 0 ]]; then
reason=$(echo "$output" | tail -n 20)
jq -n --arg r "$reason" \
'{decision: "block", reason: ("affectedスコープの検証に失敗しました: " + $r)}'
fi
exit 0--base=main は既定値と同じですが、明示しておくと読み手に意図が伝わります。デフォルトブランチがmainでない構成では、ここを実際のブランチ名に置き換えます。
スクリプトの出力を単体で確かめる
フックはClaude Codeを起動しなくても、標準入力にJSONを流せば動作を見られます。npx を「失敗を返すだけのスタブ」に差し替え、上のスクリプトを2通りの入力で動かした結果が次のとおりです。
# stop_hook_active が false のとき(スタブの nx は失敗を返す)
{
"decision": "block",
"reason": "affectedスコープの検証に失敗しました: > nx run shared-ui:test\nFAIL shared-ui src/button.spec.ts\nNX Ran target test for 2 projects. 1 failed"
}
exit=0
# stop_hook_active が true のとき
(出力なし)
exit=0失敗時もスクリプト自体は exit 0 で終わり、差し戻しの指示は標準出力のJSONだけで伝えています。reason の改行は \n にエスケープされ、末尾20行がそのまま入ります。ここに実際のテスト失敗のログが入るので、Claudeは何を直せばよいかをそのまま読み取れます。
上限の8回は、環境変数 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で引き上げられます。ただし素通りさせる設計では、差し戻し後にClaudeが直した結果は、そのターンでは再検証されません。再検証は次の応答の Stop で行われます。差し戻した直後の修正まで確かめたい場合は、同じスクリプトで stop_hook_active を見ずに回し、上限に任せる構成もあります。その場合、直らない失敗は上限の8回まで差し戻しが続きます。この上限は 0 を設定すると無効になるので、上げすぎや無効化は、直らない失敗で差し戻しが止まらなくなる設定になります。
Stopフックには、エラー扱いにならない差し戻しの手段もあります。hookSpecificOutput.additionalContext で続行の指示を返す方法で、stop_hook_active と8回の上限による保護は同じようにかかります。
PostToolUse・Stop・FileChangedの使い分け早見表
3つのフックはいずれもファイル変更に関われますが、affectedスコープの検証に向くかどうかは用途によって変わります。
| フック | 発火タイミング | affected検証への向き不向き |
|---|---|---|
| PostToolUse | 発火タイミングEdit/Write成功直後、1回の編集ごと | affected検証への向き不向き△ 編集回数だけ重複実行され、Bash経由の変更は対象外 |
| Stop | 発火タイミング応答完了時、編集手段を問わず1回 | affected検証への向き不向き◎ 累積した変更を1回でまとめて検証、失敗は差し戻せる |
| FileChanged | 発火タイミング監視ファイルの変更検知(要ファイル名列挙) | affected検証への向き不向き✕ 包括監視ができず、失敗を差し戻せない |
編集の合間に素早くフィードバックを返したいなら PostToolUse が有効な場面もあります。その場合も、affectedの計算自体は毎回走ります。セッション単位でまとめて検証したいなら Stop を選びます。
PostToolUse を選ぶ場合の設定は、matcher に Edit|Write を指定するだけで、スクリプトはStopの例をそのまま流用できます。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-nx-affected.sh"
}
]
}
]
}
}10ファイルを連続で編集したセッションの起動回数
Stop
1回
応答の終わりに1回だけ検証
PostToolUse(Edit|Write)
10回
編集のたびにスクリプトを起動
PostToolUse のスクリプトは stop_hook_active を含まない入力を受け取る点も違います。上のスクリプトを流用すると jq が null を返しますが、判定は "true" との比較なので素通りにならず、そのまま検証が走ります。ただし PostToolUse の decision: "block" は、Claudeの終了を止めません。reason がツールの結果の隣に添えられ、失敗の内容がその場でClaudeに伝わる形です。
CIのbase/head設定とローカル実行の違い
CI環境では、nx affected の base と head を明示的に指定する必要があります。GitHub Actionsなら nrwl/nx-set-shas アクションが、mainブランチ上で最後に成功したワークフローを見つけて NX_BASE と NX_HEAD を自動設定します。
- uses: actions/checkout@v7
with:
fetch-depth: 0
filter: tree:0
- run: npm ci
- uses: nrwl/nx-set-shas@v5
- run: npx nx affected -t lint test buildfetch-depth: 0 は必須です。全履歴がないと比較対象のコミットがなく、affectedの計算が成り立ちません。
基準の決め方はCIとローカルで違う
CI
nrwl/nx-set-shas が最後に成功したmain上のコミットを NX_BASE に入れます。前回のCIが成功した時点からの変更をすべて拾います。
Stopフック
baseはmain、headは現在の作業ツリーです。SHAの管理は要りません。未コミットの編集もheadに含まれるので、セッション中の変更をそのまま検証できます。
GitLab CIでのbase/head設定
GitLabにはGitHub Actionsのnrwl/nx-set-shasに相当する公式アクションがないため、CIの定義済み変数からNX_BASEとNX_HEADを組み立てます。
CI:
script:
- export NX_HEAD=$CI_COMMIT_SHA
- export NX_BASE=${CI_MERGE_REQUEST_DIFF_BASE_SHA:-$CI_COMMIT_BEFORE_SHA}
- npx nx affected -t lint test buildマージリクエストのパイプラインは差分のベースコミットと比較し、mainへのプッシュは1つ前のコミットと比較します。
CIのテスト戦略をSKILL.mdでチームに教える運用はClaude Codeモノレポのテスト戦略をSKILL.mdで教える手順で扱っています。Stopフックによるローカル検証と、CIのaffected実行を組み合わせる設計はここが起点になります。
よくあるつまずき
lock fileの変更で全プロジェクトが対象になっていないか
Nxはデフォルトで、パッケージマネージャーのlock fileが変わると全プロジェクトを影響対象としてマークします。Claude Codeが依存パッケージを追加・更新したセッションでは、affectedの絞り込みが効かず、通常の全件検証と同じ実行時間になります。
nx.json の pluginsConfig.@nx/js.projectsAffectedByDependencyUpdates を設定します。"auto" にすると、lock file内で実際に依存が変わったプロジェクトだけに絞り込めます。既定値は "all" で、プロジェクト名やglobの配列を渡せば、lock fileが変わったときに対象にするプロジェクトを指定することもできます。
{
"pluginsConfig": {
"@nx/js": {
"projectsAffectedByDependencyUpdates": "auto"
}
}
}worktree内でCLAUDE_PROJECT_DIRとcwdを取り違えていないか
Claude Codeがセッション中にworktreeへ移動すると、${CLAUDE_PROJECT_DIR} は元のプロジェクトルートを指したままになります。一方でフック入力JSONの cwd フィールドはworktreeのルートに変わります。
フックスクリプト自体は ${CLAUDE_PROJECT_DIR} から起動して構いませんが、nx affected を実行する対象ディレクトリは cwd から読み取る必要があります。前掲のスクリプトで cd "$cwd" を挟んでいるのはこのためです。
生成物やログがaffectedの判定に混ざっていないか
Nxは .gitignore に書かれたパターンを、affectedの判定から除外します。さらに .nxignore を置けば、Gitで管理するが影響判定には使いたくないファイルも除外できます。Claudeがビルド成果物やログを作業ツリーに書き出す構成では、.gitignore か .nxignore に入れておきます。入れ忘れると、無関係なプロジェクトが対象に入るおそれがあります。
Gitを使わない構成で差分の基準を見失っていないか
Nxのaffectedはデフォルトでコミット履歴の差分に依存します。Gitを使わないワークフローでは --files フラグで変更ファイルを明示的に渡す必要があり、これを忘れるとNxは差分の基準を見失います。
Stopフックのタイムアウトがaffected実行に足りているか
設定例の timeout: 120 は、affectedの対象範囲が広いと不足することがあります。command タイプのフックは timeout を省略すると600秒で打ち切られるので、省略しても極端に短くはなりません。キャッシュが効いていない初回実行や、影響範囲が想定より広がった場合は、プロジェクト規模に応じて余裕を持たせてください。フックのタイムアウトとexit codeの挙動の詳細はClaude Code Hooks実例カタログの各レシピも参考になります。
まとめ
ローカルの自動検証にはStopフックを選び、stop_hook_active で再検証を1回に抑える構成が、Nx affectedとの組み合わせでは扱いやすくなります。Edit|Write に絞った PostToolUse はBash経由の変更を拾えず、FileChanged は失敗を差し戻せません。1回の応答で両方を満たせるのは Stop です。
フックを足すときは、nx.json の projectsAffectedByDependencyUpdates とキャッシュの設定も同時に見直すと、実行時間の伸びを抑えられます。