Claude Code に長めのタスクを任せている間、ターミナルを何度も見に行っていないでしょうか。終わったのか、承認待ちで止まっているのかは、見に行かないとわかりません。
Claude Code の hooks を使うと、作業完了や承認待ちのタイミングで任意のコマンドを実行できます。この記事では、macOS の通知を出す設定と、権限リクエストを Mac のダイアログで許可・拒否する設定を、そのまま使える形で紹介します。必要なのは jq(brew install jq)だけです。
使う hook イベント
| イベント | 発火するタイミング | 主な入力 |
|---|---|---|
Stop |
Claude が応答を終えたとき(ユーザーによる中断では発火しない) |
last_assistant_message, stop_hook_active
|
Notification |
入力待ち・承認待ちなどの通知時 |
message, title, notification_type
|
PermissionRequest |
ツール実行の許可判断が必要なとき |
tool_name, tool_input
|
どのイベントにも session_id、cwd、hook_event_name などの共通フィールドが stdin の JSON で渡されます。Notification の matcher は notification_type に対して効くので、permission_prompt(承認待ち)や idle_prompt(入力待ち)に絞り込めます。
イベント名やフィールドは更新されることがあるので、最新は公式ドキュメント(https://code.claude.com/docs/en/hooks )で確認してください。
1. 最小構成: 完了と入力待ちを通知する
まずはスクリプトなしの最小構成です。~/.claude/settings.json に追加します。
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command",
"command": "osascript -e 'display notification \"作業が終わりました\" with title \"Claude Code\" sound name \"Glass\"'" }
]
}
],
"Notification": [
{
"matcher": "permission_prompt|idle_prompt",
"hooks": [
{ "type": "command",
"command": "osascript -e 'display notification \"Claude Code が待っています\" with title \"Claude Code\" sound name \"Ping\"'" }
]
}
]
}
}
既に設定ファイルがある場合は hooks キーだけをマージしてください。設定は新しく始めたセッションから反映されます。セッション内で /hooks を実行すると、読み込まれている hook を確認できます。
通知が表示されない場合は、システム設定 › 通知 で「スクリプトエディタ」の通知が許可されているか確認してください。osascript からの通知はスクリプトエディタ名義で表示されます。
2. プロジェクト名と内容を通知に入れる
複数のプロジェクトで並行して動かしていると「どれが終わったのか」がわかりません。stdin の JSON から cwd と本文を取り出して通知に入れます。
~/.claude/hooks/notify.sh:
#!/bin/bash
# Claude Code の hook 入力(stdin の JSON)から通知を出す
input=$(cat)
event=$(jq -r '.hook_event_name' <<<"$input")
project=$(basename "$(jq -r '.cwd' <<<"$input")")
case "$event" in
Stop)
title="完了: $project"
msg=$(jq -r '.last_assistant_message // "作業が終わりました"' <<<"$input" | head -c 120)
sound="Glass" ;;
Notification)
title="入力待ち: $project"
msg=$(jq -r '.message // "Claude Code が待っています"' <<<"$input")
sound="Ping" ;;
*) exit 0 ;;
esac
# 文字列はコマンド引数で渡す(引用符を含む文でも壊れない)
osascript - "$msg" "$title" "$sound" <<'APPLESCRIPT' >/dev/null
on run argv
display notification (item 1 of argv) with title (item 2 of argv) sound name (item 3 of argv)
end run
APPLESCRIPT
exit 0
ポイントは、本文を AppleScript の文字列に直接埋め込まず、on run argv で引数として渡していることです。Claude の応答には引用符やバッククォートがよく含まれるので、osascript -e "... $msg ..." のように埋め込むと壊れます。
head -c 120 はバイト単位で切るため、日本語の途中で切れることがあります。気になる場合は cut -c1-60 などに置き換えてください。
chmod +x ~/.claude/hooks/notify.sh
{
"hooks": {
"Stop": [
{ "hooks": [ { "type": "command", "command": "~/.claude/hooks/notify.sh" } ] }
],
"Notification": [
{ "matcher": "permission_prompt|idle_prompt",
"hooks": [ { "type": "command", "command": "~/.claude/hooks/notify.sh" } ] }
]
}
}
動作確認は、ダミーの JSON を流し込むのが手早いです。
echo '{"hook_event_name":"Stop","cwd":"/tmp/myapp","last_assistant_message":"テスト \"引用\" 完了"}' \
| ~/.claude/hooks/notify.sh
補足(コメントでご指摘いただきました・公式リファレンスで確認済み)
permission_promptは承認の確認が出てから約6秒キー入力がないとき、idle_promptは応答が終わって約60秒後(その間に入力がなく、バックグラウンドのサブエージェントも動いていないとき)に発火します。ターミナルの前にいると鳴らないのはこのためです。すぐに鳴らしたい場合は「3.」のPermissionRequestを使います。一方、サンドボックス内のコマンドのネットワークアクセス確認ではPermissionRequestは実行されず、permission_promptだけが拾えるので、この「2.」の通知も残しておくのが安全です。
3. 権限リクエストを Mac のダイアログで許可・拒否する
PermissionRequest hook は、stdout に JSON を返すことで許可・拒否を決められます。
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": { "behavior": "allow" }
}
}
deny の場合は message で理由を Claude に伝えられます。決定を返さなければ通常の確認フローのままです。これを使って、Mac のダイアログで答えられるようにします。
~/.claude/hooks/approve.sh:
#!/bin/bash
# PermissionRequest: Mac のダイアログで許可・拒否する。
# 30秒答えなければ何も返さず、通常のターミナルの確認に任せる
input=$(cat)
tool=$(jq -r '.tool_name' <<<"$input")
detail=$(jq -r '.tool_input.command // .tool_input.file_path // .tool_input.url // ""' <<<"$input" | head -c 300)
answer=$(osascript - "$tool" "$detail" <<'APPLESCRIPT'
on run argv
set r to display dialog (item 2 of argv) with title ("Claude Code: " & item 1 of argv) buttons {"拒否", "許可"} default button "許可" giving up after 30
if gave up of r then return "timeout"
return button returned of r
end run
APPLESCRIPT
)
case "$answer" in
許可) behavior=allow ;;
拒否) behavior=deny ;;
*) exit 0 ;; # タイムアウトなど: ターミナルの確認に戻す
esac
jq -n --arg b "$behavior" '{
hookSpecificOutput: {
hookEventName: "PermissionRequest",
decision: ({behavior: $b} + (if $b == "deny" then {message: "Mac のダイアログで拒否されました"} else {} end))
}
}'
{
"hooks": {
"PermissionRequest": [
{ "matcher": "Bash|Edit|Write",
"hooks": [ { "type": "command", "command": "~/.claude/hooks/approve.sh", "timeout": 60 } ] }
]
}
}
注意点です。
-
timeoutはダイアログの待ち時間(30秒)より長くしておきます - hook が
allowを返しても、settings の deny ルールや ask ルールは引き続き評価されます。危険なコマンドの禁止は permissions 側に書いておくのが安全です - ダイアログはフォーカスを奪うので、作業中に頻繁に出ると邪魔になります。matcher を絞るか、通知だけの 2 の構成から始めるのがおすすめです
この方法の限界
hooks だけでもかなり便利になりますが、次の点は残ります。
- 通知もダイアログも、Mac の前にいるときしか見られない
- Mac がスリープすると、そもそも Claude Code が止まる。蓋を閉じた場合も同様(
caffeinateでは蓋を閉じたときのスリープは防げないことが多い)
Lidnight で同じことをする場合
筆者は、この延長で Lidnight という Mac のメニューバーアプリを作っています。設定 › エージェント で Claude Code をオンにすると、上のような hook を代わりにインストールします(設定ファイルは事前にバックアップされ、オフにすると Lidnight の項目だけが削除されます)。
- タスクの終了や承認待ちを通知し、承認リクエストには Claude が何をしようとしているか(コマンド、編集するファイル、URL)を表示します
- 通知のボタン、メニューバー、ノッチのすぐ左に出る小さなうさぎから許可・拒否できます。ターミナルの確認もそのまま表示され、先に答えた方が適用されます
- AI の作業中は蓋を閉じてもバッテリー駆動でも Mac をスリープさせず、作業が終わるとスリープに戻ります
- Pro では ntfy アプリ経由でスマートフォンにも通知が届き、その場で応答できます。
rm -rf、git push、sudoのような元に戻しにくいコマンドは Mac でのみ許可できます
Mac 上での通知・承認と蓋を開けたままの AI Auto モードは無料、蓋を閉じても続けることやスマートフォン連携などは Pro(買い切り $9.99、10月31日まで 50% オフ)で、7日間のトライアルがあります。Apple Silicon・macOS 13 以降が対象です。
brew install whereisdw/tap/lidnight
もちろん、この記事の hooks 設定だけでも日常的な「見に行く回数」はかなり減らせます。まずは 1 の最小構成から試してみてください。