SubagentStart / SubagentStop(サブエージェントスタート・サブエージェントストップ)

フック
SubagentStart / SubagentStop
サブエージェントスタート・サブエージェントストップ
Claude Codeのフック(特定タイミングで外部スクリプトを自動起動する仕組み)のうち、サブエージェント(メインのClaudeが切り出して立ち上げる子分役のClaude)が立ち上がる瞬間に発火するのがSubagentStart、その子分が処理を終える瞬間に発火するのがSubagentStop。settings.jsonに登録して使う

/agentsでカスタムサブエージェントを使った経験のある開発者で、サブエージェントの起動・完了タイミングで自動ログ・通知・テスト介入を仕込みたい人向け

Exploreや/agentsで自作したカスタムサブエージェントを本番で運用していて、子分が立ち上がるたびにログを残したい・終わるたびにSlackへ通知したい・テストが緑じゃないと子分を勝手に終わらせず修正させ続けたい、という場面で、.claude/settings.jsonのhooks欄にSubagentStartとSubagentStopをmatcher(agent typeで絞る欄)つきで登録する

SubagentStartとSubagentStopは、メインのClaude Codeが小さな子分を1人起こして仕事を投げ、その子分が仕事を終えるまでの「入り口」と「出口」に介入できる2つのフックです。中身としては、/agents で定義したカスタムサブエージェントや、組み込みの Explore Plan general-purpose が立ち上がる瞬間に SubagentStart が、その子分が処理を終える瞬間に SubagentStop が、それぞれ settings.json に書いた外部スクリプトを叩いてくれます。

使いどころとしては、「どの子分が動いたのかをログに残したい」「子分の作業が終わったら Slack に通知したい」「テストが落ちたままなら子分を勝手に終わらせず修正させたい」という、サブエージェント単位の運用フックがほぼ全部ここに集約されます。

噛み砕くと

会社で考えると、上司に当たるメインのClaudeが「ちょっとこの調査だけ頼む」と新人役のサブエージェントに仕事を渡す瞬間と、新人が「終わりました」と戻ってきた瞬間の、それぞれにベルを鳴らせる仕組みです。SubagentStart が「新人が会議室に入った瞬間のベル」、SubagentStop が「会議室から出てきた瞬間のベル」。

ベルが鳴ったときに何をやるかは自由で、ノートに「○月○日、Aさん入室」と書くだけでもいいし、出口のベルでは「報告書が雑だったらもう一回会議室に戻す」みたいな引き留めもできます。

ここがメインの Stop / StopFailure と地味に違うところで、SubagentStart / SubagentStop は「上司本人」ではなく「新人」のタイミングに反応します。両方を別々に書ければ、新人ごとの動きと上司の動きを切り分けて記録できる、というのが基本構造です。

大事な前提:matcher は「ツール名」ではなく「agent type」で絞る

このフックでいちばん引っかかるポイントは、matcher の中身です。PreToolUse や PostToolUse は Bash Read Edit のような「ツール名」で絞ります。一方、SubagentStart / SubagentStop は general-purpose Explore Plan、または /agents で自分が定義したカスタムエージェント名で絞ります。

ここで Bashpermission_prompt のような他イベント用の値を書いてしまうと、永久に1回も発火しません。「設定したのに動かない」の8割はこの取り違えです。

matcher を空、つまり "*""" や省略にすると、どの種類のサブエージェントが立ち上がっても全部発火します。最初は空運用にしてログだけ取り、種類が見えてきてから絞るのが安全です。

「cooking-blog」プロジェクトで、Exploreサブエージェントを観測してみる

世界の料理を紹介するブログ「cooking-blog」のプロジェクトで、コード調査用のサブエージェント Explore を走らせる場面を想定します。やりたいのは、Exploreが立ち上がった瞬間にローカルのログファイルへ1行追記し、終わった瞬間に Slack へ通知することです。

ステップ1: settings.json にフックを2つ登録する

プロジェクト直下の .claude/settings.json を開いて、hooks 欄に SubagentStart と SubagentStop を並べます。matcher は Explore 固定にしておきます。

{
  "hooks": {
    "SubagentStart": [
      {
        "matcher": "Explore",
        "hooks": [
          { "type": "command", "command": "./.claude/hooks/log-subagent-start.sh" }
        ]
      }
    ],
    "SubagentStop": [
      {
        "matcher": "Explore",
        "hooks": [
          { "type": "command", "command": "./.claude/hooks/notify-subagent-stop.sh" }
        ]
      }
    ]
  }
}

matcher 欄に Bashgit を入れたくなりますが、ここで入れるのはあくまで agent type です。

ステップ2: SubagentStart 側のスクリプトを書く

.claude/hooks/log-subagent-start.sh に、標準入力で渡される JSON をそのまま受け取って、ログ1行追記するだけの中身を入れます。

#!/bin/bash
INPUT=$(cat)
AGENT=$(echo "$INPUT" | jq -r '.agent_type')
SESSION=$(echo "$INPUT" | jq -r '.session_id')
echo "[$(date '+%Y-%m-%d %H:%M:%S')] START agent=$AGENT session=$SESSION" \
  >> ./.claude/logs/subagent.log

標準入力で受け取る JSON には hook_event_name session_id cwd agent_type が入ってきます。agent_type を見れば「いま立ち上がったのは Explore なのか security-reviewer なのか」が判別できます。

ステップ3: SubagentStop 側のスクリプトを書く

.claude/hooks/notify-subagent-stop.sh では、Slack webhook を1回叩いて完了通知を送ります。webhook URL は SLACK_WEBHOOK_URL という名前で、パソコンが起動時に覚えている設定値の中に入れている前提です。

#!/bin/bash
INPUT=$(cat)
AGENT=$(echo "$INPUT" | jq -r '.agent_type')
curl -s -X POST -H 'Content-Type: application/json' \
  -d "{\"text\":\"subagent $AGENT finished on cooking-blog\"}" \
  "$SLACK_WEBHOOK_URL"

ここで exit 0 で抜ければ、サブエージェントはそのまま正常終了します。Slackが落ちてても本筋は止めない方針です。

ステップ4: 実行権限を付けて、Claude Code 内で Explore を呼ぶ

2本のスクリプトに実行権限を付けます。

$ chmod +x ./.claude/hooks/log-subagent-start.sh
$ chmod +x ./.claude/hooks/notify-subagent-stop.sh

その上で Claude Code 上で「recipes/ 配下のディレクトリ構造をExploreで調べて」と頼みます。Explore サブエージェントが立ち上がった瞬間に subagent.log に「START agent=Explore」が記録され、調査が終わった瞬間に Slack に「subagent Explore finished on cooking-blog」が流れます。

ステップ5: ここで初心者がやりがちな勘違いを潰しておく

「メインの会話でも Slack通知が来てしまった」と思った場合、それはたぶん Stop フックの方を別途登録していて、メイン会話の終了でも発火している、というケースです。

逆に、メイン用に書いた Stop フックが、subagent の完了時にも勝手に動いて困ることがあります。これは仕様で、公式ドキュメントには次のように書かれています。

For subagents, Stop hooks are automatically converted to SubagentStop since that is the event that fires when a subagent completes.

つまり「Stopにだけ書いた処理が、subagent終了時にも勝手に走る」のは公式仕様です。subagent側で別処理にしたければ、SubagentStop 側を明示的に書いて分岐させる必要があります。

ステップ6: SubagentStop で「テスト落ちたまま終わらせない」を仕込む

運用が進んできたら、SubagentStop 側で「テストが緑じゃないなら subagent を終わらせない」介入も入れられます。スクリプトの末尾を次のように変えます。

#!/bin/bash
INPUT=$(cat)
if ! npm test --silent > /dev/null 2>&1; then
  cat <<'EOF'
{"decision":"block","reason":"npm test が落ちています。修正してから完了させてください"}
EOF
  exit 0
fi
exit 0

JSON で decision: "block"reason を返すと、subagent はそこで終了できず、reason に書いた指示が subagent 側の文脈に追加されて、修正を続けるよう促されます。これは SubagentStop だけにできる芸当で、SubagentStart 側ではどう書いても止められません。

つまり SubagentStart / SubagentStop は何をしてくれるのか

  • やってくれる: サブエージェントが立ち上がる瞬間・終わる瞬間に、好きな外部スクリプトを発火させる。agent type で絞り込めるので「Exploreが起きたときだけ」みたいな限定運用ができる。SubagentStop は decision: "block" で終了を引き留められる
  • やってくれない: ツール単位の介入。これは PreToolUse / PostToolUse の仕事。permission ダイアログのカスタマイズも別の話で、Notification フックが担当する。さらに SubagentStart 側からの停止もできない。exit 2 を返しても止まらず、stderr がユーザーに見えるだけで止まらない
  • 意味が薄い場面: そもそも /agents を一切使っていないプロジェクト。メインのClaude Codeだけで完結している運用では、SubagentStart も SubagentStop も一度も発火しない

使いどころ3シナリオ(具体題材で再現)

シナリオ1: cooking-blogでExploreの動きを観測する

料理ブログを開発中、コード調査タスクは全部 Explore に投げている。けれどメイン会話のログだけ見ても「いつExploreが何回起きたのか」が見えづらい。そこで SubagentStart 側で ./.claude/logs/subagent.log に1行追記、SubagentStop 側で処理時間を記録すると、1日の終わりに「Exploreが12回起動、平均45秒」みたいな観測値が取れるようになる。matcher は Explore 固定にしておけば、他の組み込みサブエージェントに影響しない。

シナリオ2: 家計簿アプリ開発で「テストが緑じゃないと subagent を終わらせない」

家計簿アプリの開発で、機能追加用のカスタムサブエージェント feature-implementer/agents から定義している。実装させたあと、子分が「終わりました」と戻ろうとした瞬間に npm test を走らせ、落ちていたら decision: "block"reason: "テストが赤です。失敗ケースを直してください" を返す。subagent は終了せず、reason に書いた指示を追加文脈として受け取り、修正を続ける。これは SubagentStop だからこそ書ける介入で、PostToolUse では止められない位置にある。

シナリオ3: OSSをコピーしてきた直後に「security-reviewer 起動を全件通知」

仕事で複数のOSSを git clone でローカルにコピーして触っているチームで、セキュリティレビュー用のカスタムサブエージェント security-reviewer を共通化している。SubagentStart 側で matcher を security-reviewer に固定し、起動のたびに Slack の #sec-review チャンネルへ「誰がいつどのプロジェクトでレビューを始めたか」を流す。observable運用が一気に進んで「いつの間にかレビューされていた」がなくなる。SubagentStart は止められない代わりに、観測専用と割り切ると素直に使える。

初心者が踏みやすい落とし穴

  • matcher にツール名を書いて1度も発火しない。PreToolUseと同じノリで BashRead を入れると、SubagentStart / SubagentStop は永久に動かない。入るのは general-purpose Explore Plan、または /agents で作ったカスタム名のみ
  • SubagentStartで止めようとして失敗する。exit code 2 で抜けても公式の表記は「Shows stderr to user only」で、サブエージェントはそのまま起動する。止めたい場合は SubagentStart ではなく、起動前のチェックは別の仕組み、つまり PreToolUse などで書く必要がある
  • Stopにだけ書いた処理が subagent 終了時にも勝手に動く。公式仕様で Stop は subagent 完了時に自動的に SubagentStop として走る。subagentに走らせたくない処理を Stop に書いている人は、agent_type が入っているかどうかで分岐させて回避する
  • SubagentStop の decision: "block" を JSON にせずプレーンテキストで返してしまう。標準出力に流すのは {"decision":"block","reason":"..."} の JSON のみで、ただの文字列を書いても無視される
  • スクリプトに実行権限を付け忘れて静かに失敗するchmod +x していない sh ファイルは、フックから呼ばれても起動せず、ログが何も出ない。「設定したのに動かない」のもう1つの定番原因
  • SubagentStart で重い処理を書いて、サブエージェントの体感速度を遅らせる。SubagentStart は subagent 起動の手前で同期実行される。10秒かかる処理を書くと、Exploreが立ち上がる前に毎回10秒待たされる。重い処理は SubagentStop 側、または非同期化で逃がす
  • SubagentStop の block を多用しすぎて subagent が永久ループする。テスト失敗で block、また失敗で block、を何回も繰り返すと subagent が抜け出せなくなる。block の回数上限や、特定条件以外では通す閾値を自前で持っておく

書き方

// .claude/settings.json
{
  "hooks": {
    "SubagentStart": [
      {
        "matcher": "Explore",  // agent typeで絞る(ツール名ではない)
        "hooks": [
          { "type": "command", "command": "./.claude/hooks/on-start.sh" }
        ]
      }
    ],
    "SubagentStop": [
      {
        "matcher": "Explore",
        "hooks": [
          { "type": "command", "command": "./.claude/hooks/on-stop.sh" }
        ]
      }
    ]
  }
}

やってみるとこうなる

入力

# SubagentStartスクリプトに標準入力で渡されてくるJSON
{
  "session_id": "abc123",
  "transcript_path": "~/.claude/projects/cooking-blog/transcript.jsonl",
  "cwd": "~/projects/cooking-blog",
  "hook_event_name": "SubagentStart",
  "agent_type": "Explore"
}

出力例

# SubagentStop で「終わらせない」介入を返す場合の標準出力
{
  "decision": "block",
  "reason": "npm testが赤のままです。テストを通してから完了させてください"
}

# 何もしない場合はexit 0で抜けるだけでOK(標準出力に何も流さない)

このページに出てきた言葉

フック
特定のタイミングで外部スクリプトを自動的に呼び出す仕組み
サブエージェント(subagent)
メインのClaude Codeが切り出して立ち上げる子分役のClaude。別文脈で動き、結果だけメインに返す
agent type
サブエージェントの種類名。<code>general-purpose</code> <code>Explore</code> <code>Plan</code> や、<code>/agents</code>で作ったカスタム名(例: <code>security-reviewer</code>)
matcher
フック定義の「どの対象のときだけ発火させるか」を絞る欄。SubagentStart / SubagentStopではagent typeを入れる
/agents
カスタムサブエージェントを定義・編集するスラッシュコマンド。ここで作った名前がそのままagent typeになる
settings.json
Claude Codeの設定ファイル。プロジェクト直下の<code>.claude/settings.json</code>にフック定義を書く
decision: "block"
SubagentStopが標準出力に返せる特別な値。サブエージェントの終了を引き留めて、もう一仕事させる
webhook
「このURLを叩いたら通知が流れる」と決めておく仕組み。SlackやDiscordが提供している
jq
コマンドラインでJSONの中身を取り出す小さなツール

関連項目

公式ドキュメント

https://code.claude.com/docs/en/hooks

-

← 戻る