Claude Media
Agent Teams Hooksで強制する品質ゲートの仕組み

Agent Teams Hooksで強制する品質ゲートの仕組み

TeammateIdle/TaskCreated/TaskCompletedの3イベントで、テスト未通過のタスクを完了扱いにさせない品質ゲートの組み方を解説します。

はじめに — 3イベントで止められること

Agent Teamsには、teammateやタスクのライフサイクルに合わせてコマンドを自動実行できるフックイベントが3つあります。TeammateIdle・TaskCreated・TaskCompletedです。これらを使うと、「テストが通っていないのにタスクを完了扱いにする」「命名規則を無視したタスクが作られる」といった事態を、Claudeの判断任せではなく決定論的に止められます。

Claude Code Hooks完全ガイドではHooks全体の仕組みを扱っています。この3イベントのうち、teammateの動きに紐づくのはTeammateIdleだけです。TaskCreatedとTaskCompletedはタスクツールがあるセッションなら、チームを組んでいなくても発火します。発火の条件は後半の「発火しない・効かないときに確かめる4点」で扱います。

前提は2つです。Agent Teamsを有効化していること(CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)と、Hooksの設定ファイルの場所(.claude/settings.json)を理解していること。

3イベントの発火タイミングと止め方

まず3つのイベントがそれぞれ何をトリガーに発火するかを押さえます。

イベント発火タイミングブロックすると何が起きるか
TeammateIdle発火タイミングteammateがターンを終えてアイドルになる直前ブロックすると何が起きるかteammateはアイドルにならず作業を継続する
TaskCreated発火タイミングTaskCreateツールでタスクが作成されるときブロックすると何が起きるかタスク作成がロールバックされる
TaskCompleted発火タイミングタスクがcompleted状態にマークされるときブロックすると何が起きるかタスクは完了扱いにならない

3イベントともmatcherをサポートせず、条件分岐なしに毎回必ず発火します。絞り込みたいときは、スクリプトの中で入力JSONを見て判断します。

入力JSONの中身はイベントごとに違います。TeammateIdleは共通フィールドにteammate_name(と非推奨のteam_name)が加わります。TaskCreatedとTaskCompletedにはtask_idとtask_subjectが入り、task_description・teammate_name・team_nameは任意です。team_nameは将来のリリースで削除される非推奨フィールドなので、スクリプトでは使わないほうが安全です。

Step 1 — TeammateIdleでビルド成果物の存在を確認する

teammateが「作業を終えた」と判断してアイドルになる直前に、成果物が実際に存在するかを確認するフックです。

#!/bin/bash
 
if [ ! -f "./dist/output.js" ]; then
  echo "Build artifact missing. Run the build before stopping." >&2
  exit 2
fi
 
exit 0

制御方法は2通りです。

  • exit code 2: teammateはstderrのメッセージをフィードバックとして受け取り、アイドルにならず作業を続ける
  • JSON {"continue": false, "stopReason": "..."}: Stopフックと同様にteammateを完全に停止させる。stopReasonはユーザーに表示される

settings.jsonへの登録は次の形です。

{
  "hooks": {
    "TeammateIdle": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-build-artifact.sh"
          }
        ]
      }
    ]
  }
}

Step 2 — TaskCreatedでタスクの命名規則を強制する

タスクが作成されるタイミングで、タイトルの形式を検証します。チケット番号のないタスクを作らせたくない場合の例です。

#!/bin/bash
INPUT=$(cat)
TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject')
 
if [[ ! "$TASK_SUBJECT" =~ ^\[TICKET-[0-9]+\] ]]; then
  echo "Task subject must start with a ticket number, e.g. '[TICKET-123] Add feature'" >&2
  exit 2
fi
 
exit 0

ブロック方法は2通りあります。どちらを使ってもClaude Codeはタスクを削除し、フックが返したメッセージをツールのエラーとしてClaudeに返します。

  • exit code 2: stderrのテキストがそのままメッセージになる
  • JSON {"decision": "block", "reason": "..."}: reasonがメッセージになる

TaskCreatedはcontinue: falseを無視するので、ブロックしてもClaudeは作業を続けます。

Step 3 — TaskCompletedでテスト未通過のタスクを完了させない

もっとも実用性が高いのがこのフックです。タスクが完了マークされる直前にテストスイートを実行し、失敗していれば完了をブロックします。

#!/bin/bash
INPUT=$(cat)
TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject')
 
if ! npm test 2>&1; then
  echo "Tests not passing. Fix failing tests before completing: $TASK_SUBJECT" >&2
  exit 2
fi
 
exit 0

TaskCompletedは2つの状況で発火し、状況によってcontinue: falseの効き方が変わります。

くらべる

TaskCompletedが発火する2つの状況

どのエージェントでも

TaskUpdateで完了にしたとき

任意のエージェントがTaskUpdateツールで明示的にタスクを完了させたときです。exit code 2は完了を止め、stderrがモデルへのフィードバックになります。continue: falseは無視されます。

進行中のタスクあり

teammateがターンを終えたとき

teammateが進行中のタスクを抱えたままターンを終えたときです。exit code 2は同じく完了を止めます。continue: falseはStopフック同様にteammateを完全停止させます。

settings.jsonへの登録は他の2イベントと同じ形です。イベント名のキーごとにフック配列を並べれば、3つの品質ゲートを同時に運用できます。

{
  "hooks": {
    "TaskCreated": [
      { "hooks": [{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/validate-task-title.sh" }] }
    ],
    "TaskCompleted": [
      { "hooks": [{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-tests-before-complete.sh" }] }
    ],
    "TeammateIdle": [
      { "hooks": [{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-build-artifact.sh" }] }
    ]
  }
}

Claude Codeを起動せずにスクリプトを試す

フックはstdinでJSONを受け取り、終了コードで答えるだけのスクリプトです。登録する前に、サンプルJSONを流し込んで終了コードを見ておくと、ブロックが効かない原因の切り分けが楽になります。Step 2のスクリプトをvalidate-task-title.sh、Step 1のスクリプトをcheck.shとしてchmod +xした状態で試すと、次のようになります。

$ echo '{"hook_event_name":"TaskCreated","task_id":"1","task_subject":"Add login form"}' | ./validate-task-title.sh; echo "exit=$?"
Task subject must start with a ticket number, e.g. '[TICKET-123] Add feature'
exit=2
 
$ echo '{"hook_event_name":"TaskCreated","task_id":"1","task_subject":"[TICKET-123] Add login form"}' | ./validate-task-title.sh; echo "exit=$?"
exit=0
 
$ ./check.sh </dev/null; echo "exit=$?"
Build artifact missing. Run the build before stopping.
exit=2
 
$ mkdir dist && touch dist/output.js && ./check.sh </dev/null; echo "exit=$?"
exit=0

メッセージはstderrに出て、終了コードが2のときだけブロックとして扱われます。jqが無い環境ではtask_subjectが空になるので、Step 2のスクリプトは全タスクを拒否します。先にjq --versionを確かめておきます。

入力JSONにキーが無いと、jq -rは文字列nullを返します。task_descriptionのような任意フィールドを検証するときは、空文字だけでなくnullとの比較も必要です。

3イベントを組み合わせたときの流れ

3つのフックを同時に設定すると、teammateの1サイクルはおおむね次の順に進みます。

手順

teammateの1サイクルで働く3つのゲート

  1. 1

    TaskCreatedがタイトルを検証する

    タスクを作る瞬間に形式をチェックします。違反していればタスクは削除され、メッセージがツールのエラーとしてClaudeに返ります。

  2. 2

    TaskCompletedがテストを走らせる

    teammateがTaskUpdateでタスクを完了にしようとすると、テストを実行します。失敗すれば完了はブロックされ、teammateはテストの修正に戻ります。

  3. 3

    TeammateIdleが成果物を確かめる

    すべてのタスクを片付けて手を止める最後の瞬間に、ビルド成果物の存在を最終確認します。

この3段構えの利点は、どの段階で基準を外れたかが自動的に切り分けられることです。タスクの作り方か、個々の完了基準か、チーム全体の成果物か。teammateへの指示文で毎回言い聞かせなくても、同じ基準が決定論的に適用され続けます。

品質ゲートの粒度をどう決めるか

3つのイベントに何を割り当てるかは、チェックしたい対象の粒度で決めます。

チェックしたい対象割り当てるイベント理由
タスクの命名・記述ルール割り当てるイベントTaskCreated理由タスクが作られる瞬間にしか検証できない
個々のタスクの完了基準(テスト・lint)割り当てるイベントTaskCompleted理由タスク単位の粒度で検証するのに最も自然
チーム全体としての最終成果物割り当てるイベントTeammateIdle理由teammateが本当に「終わった」と言えるかの最終ゲート

TaskCompletedとTeammateIdleを両方使うと一見冗長に見えます。しかしTaskCompletedはタスク単位でテストを通す規律を、TeammateIdleは複数のタスクをまたいだ成果物(ビルド・デプロイ設定ファイル等)を確かめる役を担います。片方だけでは、タスクは全部テスト付きで完了しているのに最終成果物が揃っていない、という抜け穴が残ります。

逆にTeammateIdleだけにすると、成果物が欠けていたときにどのタスクが原因かを後から探す手間が増えます。小さく始めるならTaskCompletedだけを入れて効果を見て、足りなければ他の2つを足す順が無理のない導入です。

発火しない・効かないときに確かめる4点

つまずきの大半は、フックそのものではなく発火の前提にあります。

Taskツールがそのモデルに載っているか

TaskCreatedは、TaskCreateツールがないセッションでは発火しません。タスク管理ツール(TaskCreate・TaskGet・TaskUpdate・TaskList)は、既定ではClaude 3.x、Opus 4〜4.7、Sonnet 4〜4.6、Haiku 4.5だけに載ります。それより新しいモデルでは、次のいずれかで有効にしない限りツールが出ません。

  • CLAUDE_CODE_ENABLE_TODO_TOOLS=1を付けて起動する
  • --allowedTools TaskCreateのように、ツール名を指定して起動する
  • --toolsにタスクツールを含める

バックグラウンドセッションとクラウドセッションは、モデルを問わず最初からタスクツールが載ります。タスクツールがないセッションでは、エージェント同士が共有タスクリストの代わりにメッセージで調整します。

チームを組んでいなくても発火するイベントがある

TaskCreatedとTaskCompletedは、タスクツールを使う単一セッションでも発火します。TeammateIdleだけは、teammateがいなければ出番がありません。

Agent Teamsはインタラクティブセッションでしか動かない

teammateを起動できるのはインタラクティブなセッションだけです。-pを付けた非対話モードやAgent SDKのセッションではteammateが起動しないので、TeammateIdleの出番もありません。

スクリプトの基本動作

  • 実行権限がない: chmod +xしていないと、本来ブロックしたい場面でフックが何もせず素通りします
  • ログがstdoutに混ざる: exit code 2で返すメッセージはstderrに書きます。デバッグ用のechoをstdoutに残すと、意図しない文字列がフィードバックに紛れ込みます
  • 重いテストを毎回回す: すべてのタスク完了でフルテストを回すと、タスクの粒度が細かいチームほど待ち時間が積み上がります。対象範囲を絞ったテストコマンドへ差し替えるのが実用的です

promptフック・agentフックで置き換えるときの違い

3イベントはコマンドフックだけでなく、promptフック(LLMが判定)とagentフック(ツールを使う検証役)にも対応しています。テストコマンドで機械的に判定できない基準、たとえば「成果物が依頼の趣旨に沿っているか」を見たいときの選択肢です。

注意点は、ok: falseを返したときの挙動がコマンドフックのexit code 2と揃っていないことです。

くらべる

ok: falseのときの挙動

既定は停止

TeammateIdle

既定ではteammateが止まり、理由は警告行として表示されます。continueOnBlock: trueを付けたときだけ、理由がteammateへ返って作業が続きます。

ターン中は継続

TaskCreated / TaskCompleted

TaskCreatedと、ターン中に完了マークされたTaskCompletedは、理由がツールエラーとしてClaudeに返り、ターンは続きます。teammateの停止でTaskCompletedが発火したときは、TeammateIdleと同じく既定で停止します。

コマンドフックの感覚でTeammateIdleにpromptフックを置くと、「作業を続けさせたいのにteammateが止まる」ことになります。続けさせたいならcontinueOnBlockを明示します。

{
  "hooks": {
    "TeammateIdle": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "チームの成果物が依頼の要件を満たしているか判定してください。入力: $ARGUMENTS",
            "continueOnBlock": true
          }
        ]
      }
    ]
  }
}

LLMはJSONでokとreasonを返す必要があります。timeoutは既定で30秒です。

単一セッションのサブエージェントhookとの違い

Sub-agentsにも、個別のサブエージェント定義にPreToolUseフックを埋め込んでツール実行を制限する仕組みがあります。たとえば読み取り専用のDBクエリしか許可しないサブエージェントを、frontmatterに直接hooksを書いて作ることができます。

これはサブエージェント1体に閉じた制御です。TeammateIdle・TaskCreated・TaskCompletedはタスクのライフサイクルにかかる制御で、チーム全体で「基準を満たさないと完了させない」という規律を敷けます。個々のツール呼び出しを縛りたいならサブエージェント側のhooks、タスクの作成と完了を縛りたいなら3イベント、と使い分けます。

よくある質問

一時的にゲートを無効化したいときはどうするか

方法は2つあります。.claude/settings.jsonから該当イベントのエントリを一時的に外すか、スクリプト冒頭に[ "$SKIP_QUALITY_GATE" = "1" ] && exit 0を入れておきます。後者はSKIP_QUALITY_GATE=1 claudeのように、起動前に環境変数を設定して使います。

まとめ

導入はTaskCompletedのテスト実行から始めると、効果が一番見えやすくなります。TeammateIdleにpromptフックを置く場合は、continueOnBlockの有無で結果が変わるので、先にそこを決めておきます。

この記事を共有:XはてブLinkedIn