TaskCreated/TaskCompletedフックでタスク進捗を外部通知する
タスクの作成・完了ごとに発火するTaskCreated/TaskCompletedフックで、Slack通知や完了条件の強制を組む方法。モデルによる発火条件の違いも扱います。
TaskCreated/TaskCompletedフックとは
TaskCreatedはタスクリストに新しいタスクが作られた瞬間、TaskCompletedはタスクが完了扱いになった瞬間に発火するフックです。Claude CodeのTaskCreate/TaskUpdateツールへのフックで、進捗をSlackなどの外部ツールへ流したり、完了条件を満たすまでタスクを閉じさせなかったりする用途に使います。
どちらもmatcherを持たず、対象イベントが起きるたびに例外なく発火します。裏を返せば、タスクの出入りが多いセッションでは通知の頻度も比例して増えるということです。設計段階でその前提を織り込んでおく必要があります。
用途は大きく二つに分かれます。一つは進捗の可視化です。長時間走らせるセッションやAgent Teamsでの並行作業は、いまどのタスクが動いているのかターミナルを見ていないと分かりません。通知チャンネルへ流せば、離席していても状況を追えます。もう一つは完了条件の強制です。タスクが「完了」とマークされる直前に割り込めるので、テストが通っていない・レビューコメントに未対応といった状態でタスクを閉じさせない運用が組めます。この二つは実装の作法が違うので、この記事では両方を扱います。
どのセッションで発火するか — モデルとタスクツールの対応
このフックはTaskCreate/TaskUpdateツールが有効なセッションでしか発火しません。そしてこの2つのツールは、モデルによって既定の有効・無効が分かれています。以下の対応はClaude Code v2.1.233以降の挙動です。
| モデル | Task tool(TaskCreate等)の既定 | フックの発火 |
|---|---|---|
| Opus 4.8 / Sonnet 5 / Fable 5 / Mythos 5以降 | Task tool(TaskCreate等)の既定無効(オプトイン制) | フックの発火明示的に有効化しない限り発火しない |
| 上記4系統以外のモデル(Opus 4.7など) | Task tool(TaskCreate等)の既定有効 | フックの発火発火する |
Opus 4.8以降の系統は、チェックリストを書かなくても多段階の作業を追えるという理由でTask toolを標準搭載から外しています。有効化する経路は公式に4つあります。まず環境変数CLAUDE_CODE_ENABLE_TODO_TOOLS=1が最も手軽です。CLIから個別セッションだけ有効にするなら--allowedTools TaskCreateや--toolsでの指定も使えます。Agent SDKで組む場合はallowedToolsまたはtoolsオプションに同名のツールを渡します。
バックグラウンドセッションとClaude Code on the webでは、上記のモデル既定に関わらずTask toolが提供されます。この2つの実行環境は例外として押さえておいてください。
もう一つの落とし穴はTodoWriteとの関係です。TodoWriteはTask toolの代替として既定で無効化されており、CLAUDE_CODE_ENABLE_TASKS=0で復活させられます。しかしTodoWriteはTaskCreated/TaskCompletedとは別系統のツールなので、この環境変数でTodoWriteに戻すとフックはそもそも発火しなくなります。
サブエージェントの扱いも押さえておく必要があります。セッションがTask toolを持っていれば、そこから呼ぶサブエージェントにも同じツールが渡ります。同一プロセス内で動くAgent Teamsのteammateも同様です。ただしsplit paneで独立したプロセスとして動くteammateは話が別で、自分自身が使っているモデルの既定に従います。親セッションがOpus 4.7でTask toolが有効でも、split paneのteammateがSonnet 5であればそちら側では発火しないことがあります。
入力フィールドと発火条件
両イベントとも受け取るフィールドは同じ形です。
| フィールド | 内容 |
|---|---|
task_id | 内容対象タスクの識別子 |
task_subject | 内容タスクのタイトル |
task_description | 内容タスクの詳細説明(無いこともある) |
teammate_name | 内容作成・完了したteammate名(無いこともある) |
team_name | 内容非推奨。将来のリリースで削除予定 |
発火するタイミングには違いがあります。TaskCreatedはTaskCreateツールが呼ばれた瞬間の一択です。TaskCompletedは二通りあり、TaskUpdateツールでタスクを明示的に完了させたときと、Agent Teamsのteammateが進行中タスクを残したままターンを終えたときの両方で発火します。TaskCompletedはv2.1.33で、TaskCreatedはそのあとのv2.1.84で追加された、比較的新しいイベントです。
task_descriptionとteammate_nameは「無いこともある」フィールドです。Claudeが一行のタイトルだけでタスクを作ることは珍しくないので、通知テンプレートでtask_descriptionを前提にすると空欄が頻発します。teammate_nameはメインスレッド単独のセッションでは常に空で、Agent Teamsを組んでいるときだけ値が入ります。通知スクリプト側で// "session"のようなフォールバックを必ず用意しておくと、単独セッションとチーム運用の両方で同じフックをそのまま使い回せます。
外部通知レシピ — Slackにタスクの動きを流す
HTTPフックはフックの入力JSONをそのままPOSTするだけなので、SlackのIncoming Webhookが期待する{"text": "..."}形式には整形できません。Slackへ送るなら、コマンドフックでjqを使って本文を組み立て、curlで叩くのが確実です。ターミナルのデスクトップ通知だけで足りるなら、Notificationフックのほうが設定は簡単です。
#!/bin/bash
# TaskCreated: 新規タスクをSlackに通知
input=$(cat)
subject=$(echo "$input" | jq -r '.task_subject')
teammate=$(echo "$input" | jq -r '.teammate_name // "session"')
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "$(jq -nc --arg text "🆕 [$teammate] $subject" '{text: $text}')"
exit 0settings.json側ではasync: trueを付けて、Slackへのリクエストがブロッキングにならないようにします。通知だけが目的で完了をブロックする気がないなら、この設定が要です。
{
"hooks": {
"TaskCompleted": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notify-task-done.sh",
"async": true
}
]
}
]
}
}使い分け早見表 — 通知だけか、ゲートも兼ねるか
| 目的 | 推奨構成 | exit codeの使い方 |
|---|---|---|
| Slack等への進捗通知のみ | 推奨構成async: true | exit codeの使い方常に0。通知の成否で完了を左右しない |
| 完了条件の強制(テスト通過必須) | 推奨構成同期実行 | exit codeの使い方失敗時2でブロック |
タスク命名規則の強制([TICKET-123]等) | 推奨構成同期実行 | exit codeの使い方不一致で2、TaskCreated側で使う |
| 監査ログの記録のみ | 推奨構成async: true | exit codeの使い方常に0。ログ書き込み失敗もタスク進行を止めない |
async hookはdecision controlを持ちません。async: trueを付けた瞬間、その中でexit 2を返しても完了のブロックも作成の取り消しも起きません。通知とゲートを同じスクリプトで両方やろうとすると、この非対称に必ずつまずきます。
TaskCreatedとTaskCompletedでブロックの効き方が違う
TaskCreatedをexit 2または{"decision": "block"}で止めると、Claude Codeはタスクを削除してエラーメッセージをClaudeへ返します。continue: falseは無視され、Claudeはそのまま作業を続けます。
TaskCompletedはもう一段複雑です。exit 2は常にタスクの完了を止め、stderrの内容がClaudeへのフィードバックになります。{"continue": false, "stopReason": "..."}はteammateがターンを終えたことがトリガーの場合にのみteammate全体を止め、TaskUpdateツール自体がトリガーの場合は無視されます(この場合もexit 2は変わらず効きます)。
テストが通るまで完了させたくないときは、こう書きます。
#!/bin/bash
input=$(cat)
subject=$(echo "$input" | jq -r '.task_subject')
if ! npm test 2>&1; then
echo "テストが通っていません。完了前に修正してください: $subject" >&2
exit 2
fi
exit 0Agent Teamsで複数teammateが動くときの挙動
teammateを3人立てて機能実装を分担させるようなセッションでは、TaskCreated/TaskCompletedはteammateごとに個別に発火します。誰が何を担当しているかをリーダー役のセッションだけで把握したい場合、この2つのフックをチャンネル通知に繋いでおけば、各teammateのターンをいちいち覗きに行かずに済みます。
注意したいのは、teammateがタスクを完了扱いにせずターンを終えるケースです。この場合もTaskCompletedは発火しますが、トリガーはTaskUpdateツールではなく「ターン終了」そのものです。前述のとおりこのときだけcontinue: falseが効き、teammate全体を止められます。逆にTaskUpdateで明示的に完了させたときはcontinue: falseを送っても無視されるので、完了そのものをやり直させたいならexit 2で統一するのが安全です。
よくあるつまずき
セッションを跨いで運用する前に、この5つは事前に確認しておくと事故を防げます。
- モデルの既定を確認しないまま組む: Opus 4.8やSonnet 5では標準でTask toolが無効なので、フックを設定しても何も起きない状態から始まりがちです
CLAUDE_CODE_ENABLE_TASKS=0でTodoWriteに戻す: この設定はタスク管理をTodoWriteへ切り替えるので、フック自体が発火しなくなります- async hookにexit 2でゲートを期待する: 非同期実行はdecisionフィールドを一切見ません。ブロックが必要なスクリプトは同期実行にします
team_nameをチーム識別に使う: 非推奨フィールドで将来のリリースで削除予定です。チームを識別したいならteammate_nameとセッション側の情報を組み合わせます- 別プロセスのteammateも同じフックを継承すると思い込む: split paneで動くteammateは別のClaude Codeプロセスなので、自分自身のモデルとツール構成に従います。in-processのteammateだけが親セッションの構成を継承します
まとめ
TaskCreated/TaskCompletedはタスクの出入りをそのままフックに変換する仕組みです。単純なSlack通知ならasync: trueで気楽に組めますが、完了条件を強制したいなら同期実行に切り替え、exit 2の意味がイベントごとに違うことを踏まえて書く必要があります。Opus 4.8以降のモデルではTask tool自体がオプトイン制になっている点は、フックを組む前に必ず確認しておく価値があります。
導入コストは低い部類のフックです。既存のhooks実例カタログにあるSubagentStopの通知レシピと組み合わせれば、サブエージェント単位・タスク単位の両方の粒度で進捗を追える体制が組めます。まずはasync: trueの通知だけを入れて動きを確認し、必要になった時点で完了条件のゲートを追加する順番が、事故なく組み替えられる進め方です。
よくある質問
TodoWriteでもTaskCreated/TaskCompletedは発火しますか
発火しません。TodoWriteはTaskCreate/TaskUpdateとは別系統のツールで、CLAUDE_CODE_ENABLE_TASKS=0でTodoWriteへ切り替えると、このフックは対象イベントを失って発火しなくなります。
サブエージェント内でタスクを作成してもフックは発火しますか
発火します。親セッションがTask toolを持っていれば、サブエージェント自身が別モデルで動いていても発火する仕様です。例外はsplit paneで動く独立プロセスのteammateだけで、こちらは自分自身のモデルでTask toolの有無が決まるため、親セッションで有効でも発火しないことがあります。
Slack通知が届かないときは何を確認すればいいですか
まずasync: trueを外して同期実行にし、stderrとexit codeを直接確認します。次にモデルがTask toolを既定無効にする系統(Opus 4.8/Sonnet 5等)でないか、--allowedToolsでオプトインしているかを見ます。
TaskCreatedをブロックすると再作成できますか
できます。Claude Codeはブロック時にタスクを削除してメッセージをClaudeへ返すだけなので、Claudeは条件を満たす形でTaskCreateを呼び直せます。命名規則の強制など、ユーザーへの確認なしで自動修正させたい用途に向いています。
teammate_nameとteam_nameはどう使い分けますか
teammate_nameは作成・完了操作を行ったteammateの名前で、通知の宛先や集計に使えます。team_nameはセッションから導出される非推奨フィールドで、将来のリリースで削除される予定のため新規の実装では参照しないほうが安全です。
PreToolUseでTaskCreateを止めるのと何が違いますか
PreToolUseはツール呼び出し全般に効く汎用の関所で、matcherにTaskCreateを指定すれば同じようにブロックできます。違いはmatcherの要不要です。TaskCreatedは最初からこのイベント専用なのでmatcherを書く必要がなく、他のツール呼び出しと条件が混線する心配もありません。命名規則の強制のようにタスク作成だけに閉じたロジックを書くなら、TaskCreatedのほうが設定がシンプルになります。