概要: テストフックとは、テストや lint のコマンドを AI コーディングエージェントのワークフロー内のイベントに紐付ける仕組みです。イベントが発生すると、エージェントはそのコマンドを自動的に実行します。コマンドが失敗すると、エージェントのアクションはブロックされます。既存のテストコマンドをエージェントのライフサイクルフックに組み込めば、コードが CI に届く前に、確実に実行されるローカル検証を実現できます。
AI コーディングエージェントがコードを書くスピードは非常に速く、そのたびに手を止めて手動でテストを実行していては作業の流れが途切れてしまいます。すぐに思いつく解決策は、エージェントにテストを実行するよう指示することです。しかし、それでは実行が安定しません。CLAUDE.md やシステムプロンプトに指示を書いておけば、たいていはうまくいきます。ところが、長いセッションの途中やコンテキストの圧縮後、あるいは複数ファイルにまたがる複雑な変更の最中には、「たいていうまくいく」では不十分になります。
必要なのは、コードを CI にプッシュしてより包括的な検証を行う前に、反復作業の中で軽量なテストを確実に実行する仕組みです。それを実現するのがテストフックです。テストフックは、既存のテストや lint のコマンドをエージェントのライフサイクルに組み込み、プロンプトの内容やセッションの長さ、エージェントの判断に関係なく、毎回コマンドが実行されるようにします。テストフックがなければ、失敗が最初に表面化する場所は CI になります。その頃にはすでに別の作業に移っており、エージェントがコミット前に検出できたはずの問題をデバッグするために、頭を切り替えて戻ってこなければなりません。
この記事では、テストフックの仕組み、ワークフローにテストフックを追加する理由と方法、そして CircleCI の Chunk CLI を使ってそのプロセス全体をシンプルにする方法を説明します。
テストフックの仕組み
テストフックは、3つの主要な要素で構成されています。
-
イベント: エージェントのライフサイクルにおける、特定の決まったタイミングです。たとえば Claude Code には、
PreToolUse(エージェントがアクションを実行する前)、PostToolUse(実行した後)、Stop(作業を終えたとき)といったイベントがあります。Cursor にも同様のライフサイクルイベントが用意されています。これらは LLM の判断を介さない、決まった発火ポイントです。イベントが発生すると、フックが実行されます。 -
コマンド: エージェントに実行させたい任意の標準 CLI ツールです。テストランナー、リンター、コードレビュー用のスクリプトなどが該当します。フックが新しいテストを生成したり、新しいチェックを作成したりすることはありません。すでにあるものを、確実に実行させるだけです。
-
ブロック動作: フックを強制力のある仕組みにしているのがこの要素です。コマンドが 0 以外の終了コードで終了すると、エージェントのアクションはブロックされます。エージェントは失敗を認識してエラー出力を読み、先に進む前にコードを修正しなければなりません。
Claude Code の .claude/settings.json を使った実際の例は次のとおりです。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx eslint --no-warn-ignored \"$CLAUDE_TOOL_INPUT_FILE_PATH\""
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "npm test"
}
]
}
]
}
}
この2つのフックは、エージェントのライフサイクルの中でそれぞれ異なる役割を担っています。1つ目は、ファイルを編集するたびに ESLint を実行し、エージェントがまだそのファイルを扱っている間にスタイルや構文の問題を検出します。2つ目は、エージェントが作業を終えた時点でテストスイートを実行し、テストが失敗した場合は作業の完了をブロックします。
テストにおいてフックが特に重要になるライフサイクルイベントは、大まかに次のように分けられます。
-
ファイルの編集後。 すぐに lint を実行します。エージェントはちょうどファイルを作成または変更したところなので、この時点で問題を検出すれば、別の作業に移った後ではなく、コンテキストを保ったまま修正できます。
-
完了の直前または完了時。 テストスイート全体、または影響を受ける一部のテストを実行します。テストが失敗すると、エージェントは出力を確認して修正を繰り返します。ここが強制力を発揮する最も重要なポイントです。このゲートを通過したコードは、検証済みのコードです。
-
セッション終了時。 より広範なチェックを実行します。ここでは AI コードレビューのステップが効果を発揮します。サブエージェントがセッション全体の差分をチームの基準と照らし合わせてレビューし、開発者がセッションを閉じる前に問題を指摘します。
重要なのは、これらのフックがエージェントのコンテキストウィンドウや指示、判断に依存しないという点です。フックはモデルの外側にあります。ファイル書き込み時の PostToolUse フックは、git の pre-commit フックがコミットのたびに実行されるのと同じように、ファイルが書き込まれるたびに実行されます。エージェントがフックをスキップしたり、忘れたり、不要だと判断したりすることはできません。
Claude Code のフックイベントの詳細なリファレンスについては、フックガイドとフックリファレンスを参照してください。Cursor にも、同様のライフサイクルモデルを持つ独自のフックシステムがあります。実際の動作については、GitButler による詳細な解説が実践者向けの優れた記事です。
なぜ今これが重要なのか
開発者は何十年も前から、コミット前にテストを実行してきました。今との違いは、コードを書く主体自身がテストを実行し、失敗に応じて修正を繰り返すというサイクルを、自動的かつ短い間隔で回せるようになったことです。テストフックを今導入する価値がある理由として、次の2つの変化が挙げられます。
-
AI コーディングエージェントにライフサイクルイベントが備わった。 Claude Code は、エージェントのライフサイクル全体にわたるイベントをサポートするフックシステムをリリースしました。Cursor もそれに続いて独自のフックを実装しています。どちらも、明確に定義された特定のタイミングでエージェントの動作をプログラムから制御できる、本番環境向けのインフラです。テストフックで強制力を持たせるための基盤は、2年前には存在しませんでした。今はそれがあります。
-
コード生成の量とスピードが変わった。 経験豊富な開発者が手作業でコードを書く場合、集中したセッションで触れるファイルはせいぜい数個です。AI エージェントは、数分で数十個のファイルを生成・変更できます。リグレッションが入り込む範囲は変更量に比例して広がり、スピードが速いほど、チェックされないまま問題が積み重なっていきます。このスピードに手動レビューでは追いつけません。決まったタイミングで確実に実行される仕組みなら追いつけます。
テストフックは、AI コーディングエージェントを「コードを生成するもの」から「検証済みのコードを生成するもの」へと変えます。エージェントが担うコーディング作業が増えるほど、この違いは重要になります。
テストフックを効果的に設定する方法
フックの設定がすべて品質の向上につながるわけではありません。設計の悪い設定は、意味のある問題を検出しないまま、エージェントの作業を遅くするだけです。ここでは、役に立つテストフックの設定と逆効果になる設定の違いを説明します。
タイミングに合ったチェックを選ぶ
前のセクションの例では、ファイルを編集するたびに lint を、セッション終了時にテストスイートを実行していました。この段階的な構成は意図的なものです。常に、関連する最も軽いチェックをできるだけ早い段階で実行し、重いチェックは後に回すようにしましょう。
その理由はコンテキストにあります。エージェントがファイルを書き込んだ直後に lint エラーが表面化すれば、エージェントは直前に何をしたかを完全に把握しているため、1ステップで問題を修正できます。同じエラーがセッション終了時のチェックで、20回のファイル編集の後に表面化した場合、エージェントはコンテキストを再構築しなければならず、修正にかかるトークンと時間のコストが大きくなります。
テストの実行時間にも同じ考え方が当てはまります。テストスイートの実行に10分かかる場合、それをファイル編集のたびにブロッキングフックとして使うと、エージェントの作業がボトルネックになります。代わりに、フックの対象を絞り込みましょう。ファイル拡張子でフィルタリングして関係のないコードの変更ではチェックをスキップする、コミット時点ではスイート全体ではなく影響を受けるテストだけを実行する、包括的な実行はセッション終了時や CI に回す、といった方法があります。
無限ループを防ぐためにリトライ回数に上限を設ける
テストが失敗し続け、エージェントが同じ修正を何度も試し続けると、ループに陥ります。優れたフック設定には、連続してブロックできる回数の上限が含まれています。進展がないまま N 回リトライしたら、コードを通過させて人間のレビュー対象としてフラグを立てます。そうしなければ、エージェントは自力では解決できない問題にトークンと時間を浪費し続けることになります。
セッション終了時にレビューのレイヤーを追加する
チーム固有のパターンや規約を使った AI コードレビューのステップをセッション終了時に実行すると、テストでは検出できない問題を見つけられます。命名規則、アーキテクチャ上の判断、シニアエンジニアが重視するパターンなどです。チームの実際の PR レビュー履歴に基づき、サブエージェントがセッションの変更内容をそのパターンと照らし合わせてレビューすることで、テストカバレッジだけでは見逃してしまう品質チェックのレイヤーを追加できます。
CI はどうなるのか
テストフックと継続的インテグレーション(CI)は、それぞれ異なる目的を果たします。フックは高速なローカル検証を担当します。CI は、共有インフラを必要とする、より広範でコストの高いチェックを担当します。この2つを組み合わせることで、フックによってそもそも CI に届く壊れたコミットの数を減らせます。
ローカルのテスト、lint、レビューをすでに通過したコードをエージェントがプッシュすれば、CI は CI にしかできない作業に集中できます。サービスをまたいだ結合テスト、ステージング環境に対する E2E テスト、セキュリティスキャン、コンプライアンスゲートなどです。これらは、ローカルの検証では届かない問題を検出するチェックです。インナーループでフックによる強制がなければ、CI はエージェントがローカルで検出すべきだったユニットテストや lint チェックを再実行することになり、パイプラインの時間を浪費し、本当に CI を必要とする詳細な検証を遅らせてしまいます。
インナーループとアウターループでは、テストの役割が異なります。インナーループに自分の役割を確実に果たさせるのがテストフックです。全体のフレームワークと、CLI ツールや MCP ツールがそれぞれのループにどう対応するかについては、AI ネイティブ開発における MCP と CLI の使い分けを参照してください。
CircleCI のアプローチ
ここまで説明してきたこと(コマンドをライフサイクルに組み込む、ファイルの種類で対象を絞り込む、AI レビューを管理する)を実現するには、複数の JSON/YAML ファイルを設定する必要があります。これをチーム全体で行うとなると、セットアップのコストは大きくなります。
だからこそ、私たちはオープンソースの Chunk CLI を開発しました。Chunk CLI は、こうした土台作りを代わりに行います。
プロジェクトのルートで chunk hook repo init を実行すると、Chunk CLI が設定ファイルを生成し、エージェントのライフサイクルにフックを組み込み、テストと lint のコマンドを1か所で定義できるようにします。
execs:
tests:
command: "go test ./..."
fileExt: ".go"
lint:
command: "golangci-lint run"
timeout: 60
fileExt フィールドは、エージェントが一致するファイルを変更した場合にのみフックが発火するように対象を絞り込み、フィードバックループを高速に保ちます。Chunk はフックイベント間の状態を管理するため、テスト、lint、レビューのチェックが互いに競合することなく、正しい順序で実行されます。
Chunk CLI が土台作り以上の価値を発揮するのが、AI コードレビューのレイヤーです。build-prompt コマンドは、GitHub の組織内の PR レビューコメントを分析し、チームの実際のレビュー基準に合わせたコンテキストプロンプトを生成します。このプロンプトを使って、セッション終了時にレビュー用のサブエージェントが動作し、チームの実際のレビュー履歴から導き出したパターンと照らし合わせて、セッションの変更内容をチェックします。その結果、シニアエンジニアが実際にコードをレビューするやり方を反映したレビューのレイヤーが、フックのライフサイクルの一部として自動的に実行されます。
Chunk CLI は、ローカル開発と、より重い作業をクラウドで処理する CircleCI の Chunk CI/CD エージェントとの橋渡しもします。エージェントの作業を何度もブロックする不安定なテストのように、より詳しい調査が必要な問題をフックが検出した場合は、chunk task run で Chunk のタスクを開始して処理を引き継げます。Chunk エージェントは CircleCI のクラウドインフラ上で自律的に動作し、作業が完了するとリポジトリにプルリクエストを作成します。高速で反復的な検証はローカルのフックが担当し、専用のコンピューティングリソースでバックグラウンド実行するのが適した作業は Chunk エージェントが担当します。
テストフックを始めよう
AI コーディングエージェントは、開発者が手動で検証できるよりも速くコードを生成します。そして、「テストを実行して」というプロンプトでの指示だけでは、そのギャップを埋められるほどの信頼性はありません。テストフックは、既存のテストや lint のコマンドをエージェントのワークフローの適切なタイミングで実行し、CI に届く前にローカルで失敗を検出する、確実な強制レイヤーを提供します。
Chunk CLI はオープンソースで、セットアップ全体の土台を作成します。より重い作業をクラウドで処理する Chunk エージェントを利用するには、無料の CircleCI アカウントに登録してください。
よくある質問
AI ネイティブ開発におけるテストフックとは何ですか? テストフックとは、Claude Code や Cursor などの AI コーディングエージェントのライフサイクルイベントに登録されたコマンドです。そのイベントが発生すると(たとえば、ファイルの編集後やエージェントが作業を完了する前)、コマンドが自動的に実行されます。コマンドが失敗すると、エージェントのアクションはブロックされます。フックを使えば、エージェントが覚えていることや開発者が指示することに頼らずに、ローカルでのテスト、lint、コードレビューを強制できます。
テストフックは git フックとどう違いますか? git フックは、git のワークフローの特定のタイミング(pre-commit、pre-push)で実行されます。AI ネイティブ開発におけるテストフックは、エージェントのワークフローのタイミングで実行されます。エージェントがファイルを編集した後、エージェントがコミットする前、エージェントがセッションを終了したときなどです。対象となる範囲が異なります。エージェントは、1回のコミットまでに数十個のファイルを編集することがあります。テストフックを使えば、コミットの区切りだけでなく、編集のたびにチェックを強制できます。さらに、失敗の出力をエージェントにフィードバックするため、エージェントはコードを修正して再試行できます。これは git フックにはできないことです。
テストフックは CI/CD でのテストに取って代わるものですか? いいえ。テストフックはインナーループの検証を担当します。つまり、コードがプッシュされる前にリグレッションを検出する、高速なローカルチェックです。CI はアウターループのテストを担当します。結合テスト、E2E テスト、セキュリティスキャン、コンプライアンスチェックなど、共有インフラを必要とするものです。テストフックによって CI に届く壊れたコミットの数が減るため、パイプラインの時間の無駄が減り、すでにリリースしたコードのために頭を切り替えて戻ってくることも少なくなります。
テストフックをサポートしている AI コーディングエージェントはどれですか? 最も成熟したフックシステムを備えているのは Claude Code で、ファイルの編集、コマンドの実行、エージェントの作業完了、セッションの開始などを含むライフサイクルイベントに対応しています。Cursor もバージョン 1.7 で同様のライフサイクルモデルを持つフックをリリースしました。どちらもブロック動作と構造化された出力をサポートしています。具体的なイベント名や設定形式は異なりますが、考え方は同じです。