本記事ではGemini CLIの処理完了や承認待ちを通知センターでお知らせする設定について書きます。
長時間実行されるタスクや、ツール実行の承認待ちでGeminiの手が止まるのを防ぎます!
はじめに
私は普段メインにClaude Codeを使って開発をしていますが、Claude Codeがユーザーの承認待ちになった時や、応答が完了した時にMacの通知がくるように設定しています。
作業に集中しているとClaude Codeが止まってしまったのに気づかず、Claude Codeをフルに活用できないことがあるためです。
Gemini CLIでも同じように通知をしたかったのですが、少し前までGemini CLIにはHooks機能がなく、プロンプトで通知を送ってもらうようにするしかありませんでした。
しかし、この方法だと確実に通知するわけではなく、いまいち信頼性に欠けました。
以下のissueを見るとClaude Codeのようなhooksを待ち望んでいるユーザーは結構いたようです。
Feature Request: Implement a Hooks System for Custom Automation and Workflow Integration · Issue #2779 · google-gemini/gemini-cli
2026/1/28にGemini CLIもhooksがリリースされていたので、これでClaude Codeのような通知が実現できるのではないかと思い、Gemini CLIを使って実装してみることにしました。
Tailor Gemini CLI to your workflow with hooks - Google Developers Blog
Gemini CLI hooks | Gemini CLI
通知の具体例
画像のような通知がくるようになりました。
会話のはじめのプロンプトや、開始からの時間、ラリー回数、通知時間などを出すようにしてみました。

動作環境
この記事の内容は以下の環境で検証しました。
| 項目 | バージョン | 備考 |
|---|---|---|
| macOS | macOS 15.0+ (Sequoia) | 通知センターが利用できる環境なら概ねOK |
| zsh | 5.9 | arm-apple-darwin24.2.0 |
| Gemini CLI | 0.27.4 | 最新版推奨 |
| jq | 1.7.1 | JSONパースに必須 |
| terminal-notifier | 2.0.0 | Mac標準の osascript で代用可能 |
※ terminal-notifier は必須ではありませんが、通知クリックでターミナルに戻る機能を使いたい場合に推奨します。
設定手順
🚀 1. 必要コマンドのインストール
まずは必要なツールをHomebrewでインストールします。
brew install jq
# 以下のterminal-notifierは任意ですが、あると便利です
brew install terminal-notifier
📜 2. 通知スクリプトの配置
通知処理を行うスクリプトを作成します。
今回は ~/.gemini/hooks/notification.sh に配置することにします。
# ディレクトリ作成
mkdir -p ~/.gemini/hooks
以下の内容を ~/.gemini/hooks/notification.sh として保存し、実行権限を付与してください。
このスクリプトは、Gemini CLIから渡されるJSONデータを解析し、OSの通知センターへ送信する役割を担います。
#!/bin/bash
# ==============================================================================
# Gemini CLI Notification Hook
# ==============================================================================
# 目的: Gemini CLIの処理完了や承認待ちをOSネイティブ通知で受け取る
# 依存: jq (必須), terminal-notifier (推奨)
# ==============================================================================
# --- 通知関数 (環境に合わせて最適な方法を選択) ---
notify() {
local title="$1"
local message="$2"
local sound="${3:-default}"
# terminal-notifierがインストールされている場合 (リッチな通知)
if command -v terminal-notifier &> /dev/null; then
# 現在アクティブなアプリ(ターミナル)のBundle IDを取得
local bundle_id="${__CFBundleIdentifier}"
if [[ -z "$bundle_id" ]]; then
bundle_id=$(osascript -e 'tell application "System Events" to get bundle identifier of first application process whose frontmost is true' 2>/dev/null)
fi
# 取得失敗時のフォールバック
# com.mitchellh.ghostty の部分は、お使いのターミナルに合わせて com.googlecode.iterm2 や com.apple.Terminal などに変更してください
[[ -z "$bundle_id" ]] && bundle_id="com.mitchellh.ghostty"
terminal-notifier -title "$title" \
-message "$message" \
-sound "$sound" \
-activate "$bundle_id" \
-ignoreDnD
else
# 標準のAppleScriptを使用 (フォールバック)
osascript -e "display notification \"${message}\" with title \"${title}\" sound name \"${sound}\""
fi
}
# --- 時間フォーマット関数 ---
format_duration() {
local total_seconds=$1
local hours=$((total_seconds / 3600))
local minutes=$(((total_seconds % 3600) / 60))
local seconds=$((total_seconds % 60))
if [[ ${hours} -gt 0 ]]; then
echo "${hours}h${minutes}m"
elif [[ ${minutes} -gt 0 ]]; then
echo "${minutes}m${seconds}s"
else
echo "${seconds}s"
fi
}
# ==============================================================================
# メイン処理開始
# ==============================================================================
# 引数のパース (--event notification 等)
EVENT_TYPE="after_agent"
while [[ "$#" -gt 0 ]]; do
case $1 in
--event) EVENT_TYPE="$2"; shift ;;
*) ;;
esac
shift
done
# 標準入力からJSONを受け取る
hook_input=$(cat)
# jqチェック
if ! command -v jq &> /dev/null; then
echo "Error: jq command not found."
exit 1
fi
# ------------------------------------------------------------------
# 1. 共通処理: トランスクリプトから情報を抽出して要約を作成
# ------------------------------------------------------------------
transcript_path=$(echo "${hook_input}" | jq -r '.transcript_path')
summary=""
session_duration_formatted=""
completion_time=""
USER_COUNT=0
if [[ -n "${transcript_path}" && "${transcript_path}" != "null" && -f "${transcript_path}" ]]; then
# jqでトランスクリプトJSONから一括抽出
eval $(jq -r '
.startTime as $start |
.lastUpdated as $end |
(.messages | map(select(.type == "user"))) as $user_msgs |
($user_msgs | length) as $count |
($user_msgs[0].content // "") as $first_msg |
@sh "START_TIME=\($start) END_TIME=\($end) USER_COUNT=\($count) FIRST_MSG=\($first_msg)"
' "${transcript_path}")
# 実行時間の計算
if [[ -n "${START_TIME}" && -n "${END_TIME}" ]]; then
# ISO8601形式からEpoch秒へ変換
start_str="${START_TIME%.*}"
end_str="${END_TIME%.*}"
start_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S" "${start_str}" "+%s" 2>/dev/null)
end_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S" "${end_str}" "+%s" 2>/dev/null)
if [[ -n "${start_epoch}" && -n "${end_epoch}" ]]; then
session_duration=$((end_epoch - start_epoch))
session_duration_formatted=$(format_duration ${session_duration})
# 完了時刻 (表示用)
completion_time=$(date "+%H:%M:%S")
fi
fi
# メッセージの整形とアイコン付与
if [[ ${USER_COUNT} -gt 0 ]]; then
task_type="💬"
# コマンド履歴っぽく見せる処理(コメントアウトされたコマンド部分の除去など)
FIRST_MSG=$(echo "${FIRST_MSG}" | sed 's/^[[:space:]]*#[[:space:]]*//')
# キーワードによるアイコンの出し分け
if [[ "${FIRST_MSG}" =~ ^\/ ]]; then task_type="⚡" # スラッシュコマンド
elif [[ "${FIRST_MSG}" =~ (実装|コード|作成|修正) ]]; then task_type="💻"
elif [[ "${FIRST_MSG}" =~ (検索|調べ|grep) ]]; then task_type="🔍"
elif [[ "${FIRST_MSG}" =~ (説明|解説|とは) ]]; then task_type="📚"
elif [[ "${FIRST_MSG}" =~ (テスト|test) ]]; then task_type="🧪"
fi
# 改行を削除して1行にする
clean_msg=$(echo "${FIRST_MSG}" | tr '\n' ' ' | sed 's/ */ /g')
# サフィックス情報の作成
suffix=""
[[ -n "${session_duration_formatted}" ]] && suffix=" [x${USER_COUNT}(${session_duration_formatted})]"
# 文字数制限
max_len=80
if [[ ${#clean_msg} -gt ${max_len} ]]; then
clean_msg="${clean_msg:0:${max_len}}..."
fi
summary="${task_type} ${clean_msg}${suffix}"
fi
fi
[[ -z "${summary}" ]] && summary="💭 メッセージなし"
# ------------------------------------------------------------------
# 2. イベント分岐処理
# ------------------------------------------------------------------
# A. ツール実行の承認待ち (Notification Event)
if [[ "${EVENT_TYPE}" == "notification" ]]; then
NOTIFICATION_TYPE=$(echo "${hook_input}" | jq -r '.notification_type // ""')
if [[ "${NOTIFICATION_TYPE}" == "ToolPermission" ]]; then
# どのツールが承認待ちか抽出
TOOL_INFO=$(echo "${hook_input}" | jq -r '
.details |
if .rootCommand then "Shell: " + .rootCommand
elif .tool_name then .tool_name
else "Tool Execution" end
')
current_time=$(date "+%H:%M:%S")
notify "🤖 Gemini承認待ち: ${TOOL_INFO} at 🕰️${current_time}" "${summary}" "Glass"
fi
exit 0
fi
# B. エージェント終了時 (AfterAgent Event)
if [[ "${EVENT_TYPE}" == "after_agent" ]]; then
title="🤖 Gemini完了"
[[ -n "${completion_time}" ]] && title="${title} at ${completion_time}"
notify "${title}" "${summary}" "Submarine"
fi
実行権限の付与も忘れずに行いましょう。
chmod +x ~/.gemini/hooks/notification.sh
⚙️ 3. Gemini CLI 設定ファイルへの追記
~/.gemini/settings.json を開き、hooks セクションに以下の設定を追加します。
これにより、Gemini CLIのイベント発生時にスクリプトが呼び出されるようになります。
{
// ... 他の設定 ...
"hooks": {
"Notification": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "~/.gemini/hooks/notification.sh --event notification"
}
]
}
],
"AfterAgent": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.gemini/hooks/notification.sh --event after_agent"
}
]
}
]
}
}
✅ 4. 動作確認
実際にGemini CLIでコマンドを実行して確認してみましょう。
gemini -p "sleep 3秒してから挨拶して"
3秒後に 「🤖 Gemini完了」 という通知が届けば成功です!
また、ファイル編集などの承認が必要なアクションを実行させると、承認待ちのタイミングでも通知が飛んでくるはずです。
終わりに
Claude Codeですでに通知する仕組みがあったのもあり、Gemini CLIを使ってデバッグ時間含めて2〜3時間くらいで実装できました。
初めは、承認待ち通知を送るために Notification イベントでなく BeforeTool フックを使おうとしていたのですが、BeforeTool は承認待ちの前ではなく、承認後すぐに発行されるフックだったためうまくいきませんでした。承認待ちを検知するには Notification イベントの ToolPermission タイプを見るのが正解でした!
多少の試行錯誤はありましたが、AIはこうした便利ツールをあっという間に作ってくれるので、本当にいい時代になったなと感じます。
通知が来る快適さを一度体感すると、ない時代には戻れなくなります。みなさんもぜひ使ってみてください!
tmuxを使っている方はこちらもおすすめです。
【Gemini CLI × tmux】エージェントの状態をウィンドウ名に表示して、確認待ちを見逃さないハック #Mac - Qiita