Hookのexit 2とstderrを基礎から解説
Claude Code の Hook(フック)を調べていると、こんな設定をよく見かけます。
exit code を 2 にして、stderr にメッセージを書く
「これでClaudeに通知が届く」と説明されるのですが……そもそも exit code って何? stderr って何? ここが分からないと、なぜ動くのか腑に落ちませんよね。私もここでしばらくモヤモヤしていました。
今回は、この2つを料理の例えで基礎から解説します。仕組みが分かると、Hook を自分で書くときに「なぜこう書くのか」が見えてきます。
※ Hook の基本(そもそも何ができるか、デスクトップ版での設定手順)はこちらの記事にまとめています。この記事は「通知の仕組み」に絞って深掘りします。
そもそもプログラムは「3つのもの」を返す
ターミナルでコマンドやスクリプトを動かすと、実は3種類のものが返ってきます。レストランで料理を注文する場面で例えると分かりやすいです。
あなたがシェフ(=プログラム)に「オムレツ作って」とお願いしたとします。
| 返ってくるもの | 料理での例え | プログラム用語 |
|---|---|---|
| 料理そのもの | 完成したオムレツ | stdout(標準出力) |
| 報告・文句 | 「卵が足りなかったよ」 | stderr(標準エラー出力) |
| 成功/失敗のサイン | 親指👍 or 👎 | exit code(終了コード) |
この3つが別々のルートで返ってくる、というのがポイントです。1つずつ見ていきます。
stdout:普通の「結果」を出す場所
プログラムが出すメインの出力です。普段ターミナルで目にする文字は、だいたいこれです。
たとえばファイル一覧を表示すると、こんな結果が返ってきます。
記事1.md 記事2.md 画像フォルダ
この一覧が stdout。「これが結果ですよ」という正規の出口です。料理でいえば、お皿に乗って出てくるオムレツそのものですね。
stderr:エラーや「お知らせ」専用の別ルート
ここが少し分かりにくいところです。「結果」とは別に、「エラーや報告」を流すための専用の出口があります。
なぜ分けるのか? それは、「正常な結果」と「エラーメッセージ」がごちゃ混ぜにならないようにするためです。
たとえば、存在しないファイルを開こうとすると、こんなメッセージが出ます。
そんなファイルないよ
この「ないよ」は stderr から出ています。料理に例えると、オムレツが出てこなかった代わりに、「材料がなかった」という報告が別ルートで届くイメージです。
そして大事なのが、stderr は「エラー」専用に見えて、実は「お知らせ」全般に使えること。今回のHookでは、これを「Claudeへの伝言ルート」として活用します。
exit code:成功か失敗かを表す「数字」
プログラムは終わるとき、必ず1つの数字を残します。これが exit code です。文字ではなく、ただの数字です。
ルールはシンプルです。
| 数字 | 意味 |
|---|---|
| 0 | 「成功!問題なし」👍 |
| 0 以外(1, 2, 127…) | 「何か起きた」👎 |
料理の例えなら、シェフが最後に出す親指サイン。「うまくできた(0)」か「ダメだった(0以外)」かを、たった1つの数字で教えてくれます。
普段あなたは exit code を意識していないかもしれませんが、裏では必ず付いています。git push が成功したか失敗したかも、システムはこの数字で判断しているんです。
なぜ「exit 2 + stderr」でClaudeに届くのか
ここまで分かれば、本題はもうすぐです。
Claude Code には、こんな特別ルールがあります。
| exit code | Hookでの挙動 |
|---|---|
| 0 | 「成功、問題なし」→ 何も起きない |
| 2 | 「Claudeに伝えたいことがある」→ stderr の内容が Claude に届く |
| その他 | 「エラー」→ あなた(人間)に表示されるだけ |
つまり、2 は「Claude、聞いて!」という呼び鈴🔔。そして **stderr に書いた文章が「その伝えたい中身」**📄 なんです。
stderr に「テストを忘れずに」と書く
exit code を 2 にして終了
これで Claude Code が「お、exit 2 だな。stderr の中身を Claude に渡そう」と判断し、メッセージが届きます。
呼び鈴だけ鳴らしても中身がない、中身だけ書いても呼び鈴を鳴らさないと気づかれない。だから この2つはセットで意味を持つわけです。
PostToolUse と PreToolUse で意味が変わる
面白いのが、同じ「exit 2 + stderr」でも、Hookが走るタイミングで意味が変わることです。
| タイミング | exit 2 + stderr の意味 |
|---|---|
| PreToolUse(処理の前) | その操作をやめさせる🚫+理由を伝える |
| PostToolUse(処理の後) | 操作はもう済んでいるので止められない → 追加で伝えるリマインド📬 |
たとえば私は、「記事を編集したら検証を促す」Hook を PostToolUse で作っています。編集はもう成功しているので、ブロックではなく「編集したよ、検証してね」という事後のお知らせとして届く、という仕組みです。
ちなみに、実際のスクリプトはこんな感じです(一部抜粋)。
if (記事ファイルだったら) {
process.stderr.write('記事を編集しました。検証してください');
process.exit(2); // ← ここで「2」を出す
}
process.exit(0); // それ以外は何もせず素通り
編集自体は成功しているのに、わざと 2 を出してリマインドを届けている――これが「exit 2 + stderr」の正体です。
使い分けの注意:全部に付けない
ここで1つ注意点です。「exit 2 + stderr が便利だから」と、どんなHookにも機械的に付けるのはNGです。
- Claude にメッセージを届けたい/操作をブロックしたい → exit 2 + stderr
- ただ処理を走らせるだけ(ログ記録、コード整形など、通知が不要)→ exit 0(stderr は書かない)
- 条件に当てはまらないときは → 必ず exit 0 で素通り
判断基準はシンプルで、「Claudeに伝えたいことがあるか?」だけです。伝えたいことがなければ、おとなしく 0 で終わらせましょう。
まとめ
- プログラムは stdout(結果)・stderr(報告)・exit code(成功/失敗の数字) の3つを返す
- stdout = 普通の結果、stderr = エラーやお知らせの別ルート、exit code = 0なら成功・それ以外は何かあった
- Claude Code では exit 2 = 「Claudeに伝えたい」の合図、stderr = その中身。だからセットで使う
- PreToolUse なら「ブロック」、PostToolUse なら「リマインド」と、タイミングで意味が変わる
- 通知が不要なHookは exit 0 が基本。全部に exit 2 を付けない
仕組みが分かると、コピペで設定していたHookが「なぜこう書くのか」までスッと理解できます。自分でHookを育てていくときの土台になるはずです。