Claude Code の hooks 入門:Linux/SSH 環境で“承認待ち・完了”を通知する設定【動作確認済み】

- 確認したバージョン
- Claude Code 2.1.303
- 確認日
- 2026/10/19
- 確認した環境
- Debian GNU/Linux 13(GUI なしのサーバー環境)
この記事でわかること
- hooks の設定ファイルの置き場所と、/hooks での確認方法
- 承認待ち(Notification)と完了(Stop)をスマホに通知する方法(ntfy)
- SSH 先のサーバーで notify-send を使うと何が起きるか(実際のエラー)
- 編集後の自動整形(PostToolUse + Prettier)
- 危険なコマンドのブロック(PreToolUse + 終了コード 2)と onFailure
- 実際につまずいた点のチェックリスト
目次(15項目)
Claude Code に作業を任せて別のことをしていると、「承認待ちで止まっていた」「とっくに終わっていた」ということがよくあります。hooks を使うと、こうしたタイミングで決まったコマンドを自動で実行できるので、通知を飛ばしたり、編集後に自動でフォーマットしたりできます。
日本語の解説記事は macOS(osascript や terminal-notifier)前提のものが多いので、この記事では GUI のない Linux サーバー(SSH でつないで使う想定) で、実際に設定して動作を確かめた手順をまとめます。
この記事の画面について
端末の画像は、tmux の capture-pane で取り込んだ文字と色をそのまま画像にしたものです。内容は編集していません(ntfy のトピック名だけ cc-hooks-xxxxxxxx のように伏せています)。コマンドの出力も、すべて検証時に実際に表示されたものです。
検証環境
| 項目 | 内容 |
|---|---|
| 確認日 | 2026-10-19 |
| Claude Code | 2.1.303(ネイティブ版、Claude Pro でログイン) |
| OS | Debian GNU/Linux 13(trixie)、デスクトップ通知デーモンなし |
| その他 | Node.js 20.19.2、Prettier 3.9.10、jq、curl |
| モデル | 使用量を抑えるため --model haiku で起動 |
Claude Code は更新が速く、hooks の仕様もバージョンごとに変わっています。この記事の内容は上記バージョンで確認したものです。
hooks とは
hooks は、Claude Code の決まったタイミング(イベント)で自動的に実行されるシェルコマンドです。Claude(モデル)が「実行するかどうか」を判断するのではなく、条件に合えば必ず実行されるのがポイントです。
イベントはたくさんありますが、この記事で使うのは次の 4 つです。
| イベント | 発火するタイミング | この記事での用途 |
|---|---|---|
Notification |
Claude Code が通知を出すとき(承認待ちなど) | 承認待ちを通知 |
Stop |
Claude が応答を終えたとき | 完了を通知 |
PostToolUse |
ツールの実行が成功した後 | 編集したファイルを自動整形 |
PreToolUse |
ツールを実行する前(ブロックできる) | rm -rf を止める |
ポイント
フックには、イベントの情報が JSON で標準入力に渡されます。たとえば PreToolUse で Bash を実行しようとしたときは、tool_name や tool_input.command が入っています。スクリプト側でこれを jq で読み取って処理します。
設定ファイルの置き場所
hooks は settings.json の hooks キーに書きます。どのファイルに書くかで、効く範囲が変わります(公式ガイドより)。
| 場所 | 効く範囲 | 共有 |
|---|---|---|
~/.claude/settings.json |
自分のすべてのプロジェクト | しない(このマシンだけ) |
.claude/settings.json |
そのプロジェクト | リポジトリにコミットできる |
.claude/settings.local.json |
そのプロジェクト | しない(git 管理外) |
ほかに、組織の管理設定・プラグイン・スキルやサブエージェントの frontmatter にも書けます。
今回は次のように分けました。
- 通知(Notification / Stop) … どのプロジェクトでも欲しいので
~/.claude/settings.json - 整形とブロック(PostToolUse / PreToolUse) … プロジェクトごとのルールなので
.claude/settings.json
注意
対話モードで初めて開くフォルダでは、次の「信頼してよいか」の確認が出ます。選択肢の初期位置は「No, exit」なので、そのまま Enter を押すと終了します。
一方、claude -p(非対話モード)ではこの確認が出ません。claude --help にも「-p のときは workspace trust dialog をスキップする。信頼できるディレクトリでだけ使うこと」と書かれていて、実際に今回、まだ信頼していないフォルダでも -p で実行するとプロジェクトのフックが動きました。他人のリポジトリで claude -p を実行すると、そこに書かれたフックがそのまま動くということなので注意してください。
手順 1:通知用スクリプトを作る
まず、Notification と Stop の両方から呼ぶスクリプトを ~/.claude/hooks/notify.sh に作ります。やっていることは次の 3 つです。
- 標準入力の JSON から、イベント名とメッセージを取り出す
- GUI のセッションがあれば
notify-sendでデスクトップ通知(失敗しても止めない) - ntfy のトピック名が設定されていれば、
curlでスマホに通知
#!/usr/bin/env bash
# Claude Code の Notification / Stop フックから呼ぶ通知スクリプト
# stdin でフックの JSON を受け取る
INPUT=$(cat)
EVENT=$(printf '%s' "$INPUT" | jq -r '.hook_event_name // "unknown"')
case "$EVENT" in
Notification) MSG=$(printf '%s' "$INPUT" | jq -r '.message // "入力待ちです"') ;;
Stop) MSG="応答が完了しました" ;;
*) MSG="$EVENT" ;;
esac
PROJECT=$(basename "$(printf '%s' "$INPUT" | jq -r '.cwd // "."')")
LOG="$HOME/.claude/hooks/notify.log"
echo "$(date '+%F %T') $EVENT [$PROJECT] $MSG" >> "$LOG"
# 1) デスクトップ通知(GUI セッションがあるときだけ。失敗しても止めない)
if command -v notify-send >/dev/null 2>&1 && [ -n "$DBUS_SESSION_BUS_ADDRESS$DISPLAY$WAYLAND_DISPLAY" ]; then
notify-send "Claude Code ($PROJECT)" "$MSG" >>"$LOG" 2>&1 || echo "notify-send failed" >> "$LOG"
fi
# 2) スマホ通知(ntfy)。トピック名は環境変数かファイルから読む
TOPIC="${NTFY_TOPIC:-$(cat "$HOME/.config/claude-ntfy-topic" 2>/dev/null)}"
if [ -n "$TOPIC" ]; then
curl -sS -o /dev/null -w "ntfy HTTP %{http_code}\n" --max-time 10 \
-H "Title: Claude Code ($PROJECT)" -d "$MSG" "https://ntfy.sh/$TOPIC" >> "$LOG" 2>&1
fi
exit 0
mkdir -p ~/.claude/hooks
chmod +x ~/.claude/hooks/notify.sh
Stop のメッセージを固定文にしているのは、ntfy.sh の公開サーバーにコードや会話の内容を送らないためです(理由は手順 4 で説明します)。
手順 2:Notification と Stop を登録する
~/.claude/settings.json に次のように書きます(ファイルがなければ作ります)。
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/notify.sh" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/notify.sh" }
]
}
]
}
}
matcherが空だと、すべての通知タイプで発火します。承認待ちだけにしたいなら"permission_prompt"、約 60 秒入力がないときだけなら"idle_prompt"にします。Stopは matcher に対応していないので書きません。
保存したら JSON として正しいかを確認しておきます。何も表示されなければ OK です。
jq empty ~/.claude/settings.json
手順 3:notify-send はヘッドレス環境で使えるか
公式ガイドの Linux 向けの例は notify-send です。GUI のない環境で試すとどうなるかを確認しました。
まず、notify-send 自体が入っていませんでした。
$ notify-send 'Claude Code' 'test'
notify-send: command not found
$ echo $?
127
Debian/Ubuntu では libnotify-bin パッケージに入っています。今回の環境では、最初の apt-get install が E: Unable to locate package libnotify-bin で失敗し、apt-get update してから入れ直す必要がありました。
sudo apt-get update
sudo apt-get install -y libnotify-bin
インストール後に実行すると、次のようになりました。
実際に試した結果
| 状況 | 結果 |
|---|---|
SSH 接続を想定(DISPLAY も D-Bus もない) |
Cannot autolaunch D-Bus without X11 $DISPLAY |
| 仮想ディスプレイはあるが通知デーモンがない | The name org.freedesktop.Notifications was not provided by any .service files |
| D-Bus セッションを起動しても通知デーモンがない | 上と同じエラー |
つまり、SSH 先のサーバーやコンテナでは、notify-send を入れても通知は届きません。手元の PC に通知を出す仕組みではないためです。そこで、スマホや別の PC に届く ntfy を使います。
上のスクリプトは notify-send が失敗しても終了コード 0 で終わるので、Claude Code の動作には影響しません。デスクトップ環境のある Linux で使う場合はそのまま通知が出るはずですが、今回は未確認です。
手順 4:ntfy でスマホに通知する
ntfy は、URL に HTTP で POST するだけでスマホに通知を送れるサービスです。アカウント登録なしで、公開サーバー ntfy.sh を使えます。
トピック名を決める
ntfy の公式ドキュメントにも「トピック名は公開されているので、推測されにくい名前にする」とあります。トピック名を知っている人は誰でも通知を読めるので、ランダムな文字列にします。
TOPIC="cc-hooks-$(head -c 9 /dev/urandom | od -An -tx1 | tr -d ' \n')"
echo "$TOPIC" # スマホのアプリに登録するので控えておく
mkdir -p ~/.config
echo "$TOPIC" > ~/.config/claude-ntfy-topic
curl で送れるか試す
curl -sS -w '\nHTTP %{http_code}\n' -H 'Title: Claude Code' -d 'manual test from box' "https://ntfy.sh/$TOPIC"
スマホがなくても、届いたメッセージは次のコマンドで確認できます。最初 ?poll=1 だけで試したら何も返ってこず、since=all を付けると表示されました。
curl -sS "https://ntfy.sh/$TOPIC/json?poll=1&since=all"
スマホ側の設定
ntfy の公式アプリ(Android / iOS)を入れ、「+」から同じトピック名を登録すれば、以後の通知がスマホに届きます。ブラウザで https://ntfy.sh/トピック名 を開いても一覧を見られます。この記事の検証で、実際にフックから届いた通知の一覧です。
注意:公開サーバーに送るもの
- トピック名は「パスワード」のつもりで扱い、記事やリポジトリに載せない
- 送る本文にソースコード・会話内容・秘密情報を入れない(Stop の入力には Claude の最終応答
last_assistant_messageも入っていますが、今回は送っていません) - 気になる場合は、ntfy のセルフホストやアクセス制御を検討する
今回の検証は、サーバーからの送信と、上の Web 画面・poll での受信確認までです。スマホのアプリでの受信は確認していません。
手順 5:通知が届くか確かめる
完了通知(Stop)
claude -p(非対話モード)で 1 回だけ指示を出し、終わったときに Stop が動くかを見ました。応答が終わった直後、notify.sh のログにこう記録されました。
2026-10-19 12:56:54 Stop [hooks-demo] 応答が完了しました
GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown: The name org.freedesktop.Notifications was not provided by any .service files
notify-send failed
ntfy HTTP 200
デスクトップ通知は失敗、ntfy は成功、という想定どおりの結果です。
ポイント
Stop は「タスクが全部終わったとき」ではなく、Claude が応答を終えるたびに発火します。date を実行させただけの短いやりとりでも通知が来ました。
承認待ち通知(Notification)
承認待ちの通知を試すには、Claude Code が承認を求める状況を作る必要があります。ここで 1 つ注意点がありました。
2.1.303 では、起動直後の権限モードが auto mode でした。 auto mode では Claude Code が多くの承認を自動で処理するため、承認待ちの状況を作りにくくなります。
テストでは Shift+Tab を何回か押し、画面下に ⏸ manual mode on と出る状態にしました(押すたびに manual → accept edits → plan → auto と切り替わりました)。
manual mode でも、date のような読み取りだけのコマンドは確認なしで実行されました。touch note.txt を頼むと、承認画面が出ました。
実際に試した結果
指示を送ったのが 12:59:36、notify.sh のログに記録されたのが 12:59:44 で、応答の生成時間を含めて約 8 秒後でした(公式ドキュメントでは、承認プロンプトが約 6 秒待機したときに permission_prompt が発火するとされています)。
2026-10-19 12:59:44 Notification [hooks-demo] Claude needs your permission
...
ntfy HTTP 200
デバッグログ(後述の --debug-file)にも Notification:permission_prompt [~/.claude/hooks/notify.sh] completed with status 0 と記録されていました。
idle_prompt(応答後に約 60 秒入力がない)は今回は試していません。
手順 6:/hooks で設定を確認する
対話モードで /hooks と入力すると、登録済みのフックが一覧で表示されます。
項目を選んで Enter を押すと、読み込み元のファイルも表示されます。
ポイント
2.1.303 の /hooks は 「This menu is read-only」(読み取り専用) と表示され、この画面からフックを追加・編集することはできませんでした。追加や変更は settings.json を直接編集します。セッション中に編集した場合、再起動しなくても /hooks の表示に反映されました。
手順 7:編集後に自動整形する(PostToolUse)
Claude がファイルを書いたり編集したりした直後に、Prettier をかけます。プロジェクトに Prettier を入れておきます。
npm i -D prettier
プロジェクトの .claude/settings.json に書きます(PreToolUse と同じファイルにまとめた最終形は手順 8 にあります)。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write --ignore-unknown"
}
]
}
]
}
}
注意
公式ガイドの例に --ignore-unknown を足しています。これがないと、Prettier が対応していないファイル(例:.py)を Claude が編集したときに [error] No parser could be inferred for file "/workspace/hooks-demo/t.py". というエラーになりました。
確認
Claude に「シングルクォートで 1 行に詰めた hello.js を Write ツールで作って」と頼みました。manual mode なので、書き込む前に承認画面が出ます。ここに表示されているのが、Claude が書こうとした内容(整形前)です。
承認すると書き込まれ、その直後に PostToolUse フックが動きます。Claude 自身も「PostToolUse フックがファイルを変更した」ことに気づいていました。
実際のファイルを見ると、整形されていました。
claude -p に --output-format stream-json --verbose --include-hook-events を付けて実行すると、フックの実行結果も出力に含まれます。今回は次のように記録されていました(ID などのフィールドは省略しています)。
{"type":"system","subtype":"hook_response","hook_name":"PostToolUse:Write","hook_event":"PostToolUse","output":"hello.js 31ms\n","stdout":"hello.js 31ms\n","stderr":"","exit_code":0,"outcome":"success"}
注意点として、Edit|Write の matcher は Claude が Bash でファイルを書き換えた場合には発火しません。公式ガイドでは、その場合も含めて整形したいなら FileChanged フックを使うよう案内されています(今回は未検証)。
手順 8:危険なコマンドをブロックする(PreToolUse)
PreToolUse はツールの実行前に動き、終了コード 2 で終わるとその実行をブロックできます。このとき標準エラー出力(stderr)に書いた内容が、理由として Claude に伝わります。
.claude/hooks/block-dangerous.sh を作ります。
#!/usr/bin/env bash
# PreToolUse(Bash) で危険なコマンドを止める
CMD=$(jq -r '.tool_input.command // empty')
if printf '%s' "$CMD" | grep -Eq 'rm[[:space:]]+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)'; then
echo "Blocked by hook: 'rm -rf' は禁止です。削除が必要ならユーザーに確認してください。" >&2
exit 2
fi
exit 0
chmod +x .claude/hooks/block-dangerous.sh
Claude Code に登録する前に、JSON をパイプで渡して単体で試しておくと安心です。
プロジェクトの .claude/settings.json(PostToolUse と合わせた最終形)です。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous.sh" }
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write --ignore-unknown" }
]
}
]
}
}
確認
tmpdir/ を作っておき、Claude に rm -rf ./tmpdir を実行するよう頼みました。通常の画面では「Ran 1 shell command」と短く表示されるだけなので、Ctrl+O で詳細表示に切り替えています。
実際に試した結果
実行後も tmpdir/keep.txt はそのまま残っていました。claude -p で Bash を許可した状態(--allowedTools "Bash")でも同じ結果で、PreToolUse のブロックが先に効いています。
注意
この程度の文字列チェックは、書き方を変えたコマンドまでは防げません。公式ドキュメントでも(フックの if フィルターの説明の中で)、確実な許可・拒否を強制したいときは hooks ではなく権限システムを使うよう書かれています。hooks は「うっかり」を防ぐ補助と考えるのが安全です。
手順 9:フック自体が壊れていたら?(onFailure)
ブロック用のフックで、スクリプトのパスを打ち間違えていたらどうなるでしょうか。わざと存在しないスクリプト policy-check.sh を指定して試しました。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/policy-check.sh",
"onFailure": "block"
}
]
}
]
}
}
onFailure は v2.1.295 で追加された項目で、command と http のフックに指定できます。値は "continue"(既定)か "block" です(hooks リファレンス)。echo hello-from-bash を実行させた結果はこうでした。
実際に試した結果
| 設定 | フックの結果 | Bash コマンド |
|---|---|---|
onFailure なし |
/bin/sh: 1: .../policy-check.sh: not found(終了コード 127) |
実行された(hello-from-bash が出力された) |
"onFailure": "block" |
同じく終了コード 127 | ブロックされた |
つまり onFailure を付けないと、ガード用のフックが壊れていても素通りになります。ブロック目的のフックには "onFailure": "block" を付けておくと安心です。
リファレンスによると、Stop などの一部のイベントや、async で動かすフックには onFailure は効きません。また、0 と 2 以外の終了コードやタイムアウトも「失敗」として扱われます(タイムアウトは今回未検証)。
うまく動かないときのチェックリスト
今回の検証で実際に出たエラーと、その対処です。
notify-send: command not found(終了コード 127) →libnotify-binを入れる。Unable to locate packageなら先にsudo apt-get update。Cannot autolaunch D-Bus without X11 $DISPLAY→ SSH・ヘッドレス環境です。notify-sendでは通知できないので ntfy などを使う。The name org.freedesktop.Notifications was not provided by any .service files→ 通知デーモンが動いていません。サーバーでは ntfy などを使う。settings.jsonの JSON が壊れている → 起動時なら「Settings Error … Invalid or malformed JSON」「Files with errors are skipped entirely, not just the invalid settings.」という画面が出ます。セッション中に壊した場合は画面に警告が出ず、そのファイルのフックが/hooksから消えるだけでした。claude doctorかjq emptyで確認できます(末尾のカンマが典型例です)。なおclaude --helpによると、-pモードでは検証に失敗した設定ファイルはエラー表示なしで無視されます(この挙動は未検証)。- スクリプトが見つからない(
not found、終了コード 127) → パスの打ち間違い。onFailureがないとエラーが出ても処理は続行されます。 - スクリプトに実行権限がない(
Permission denied、終了コード 126) →chmod +xする。今回はsh -cで同じ呼び出し方を再現して確認しました。 - Prettier の
No parser could be inferred→--ignore-unknownを付ける。 - 承認待ちの通知が来ない
→ auto mode だと承認画面自体が出にくい。テスト時は
Shift+Tabで manual mode にし、ファイルを変更するコマンドを頼む。 - ntfy の poll で何も返ってこない
→
?poll=1&since=allにする。
JSON をわざと壊して(末尾にカンマを入れて)確認したときの画面です。
フックの動きを詳しく見る方法
claude --debug-file /tmp/claude.logで起動すると、Hooks: PreToolUse:Bash [...] finished with status 2 (17ms)のように、フックごとの終了コードと実行時間が記録されます。claude -p "..." --output-format stream-json --verbose --include-hook-eventsなら、hook_responseに終了コード・stdout・stderr が入ります。- 対話モードでは
Ctrl+Oで詳細表示に切り替えると、フックのエラー全文が見えます。
なお、デバッグログの時刻は UTC(末尾が Z)で記録されていました。日本時間に直すには 9 時間足します。
まとめ
まとめ
- 通知は
~/.claude/settings.json、プロジェクトのルールは.claude/settings.jsonに分けると管理しやすい - SSH 先やコンテナでは
notify-sendは使えない。ntfy ならcurl1 行でスマホに届く(トピック名と送る内容には注意) - PostToolUse で自動整形、PreToolUse の終了コード 2 でブロックできる
- ガード用のフックには
"onFailure": "block"(v2.1.295 以降)を付けておく - 設定を変えたら
/hooksとclaude doctorで確認する
hooks は「必ず実行したい処理」に向いた仕組みです。決まった手順を Claude に任せたいなら Skills(SKILL.md)、外部のツールやデータにつなぎたいなら MCP も組み合わせられます。429 や 529 で止まったときの見分け方は エラー・利用制限のまとめ にまとめました。
参考
この記事は、AI を使って下書きを作り、手順とコマンドの出力・画面は筆者の検証環境で実際に実行して確認しています。アイキャッチ画像は生成画像(イラスト)で、実際の画面ではありません。