はじめに
AIコーディングに眺めのタスクを投げるとき、ユーザ許可やタスク完了のタイミングでスマホに通知できるようにすると、PCをずっと見ている必要がなくなり便利です。
claude codeでは指示待ち時にスマホ通知するノウハウが公開されていますが、copilot-cliの情報はあまりないように思えたので、実現方法をまとめました。
参考: claude codeで指示待ち時にスマホ通知する方法
やりたいこと
copilot-cliでユーザの指示を待つタイミングでスマホに通知されるようにしたいです。
具体的には以下のタイミングで通知されるようにしたいです。
- ツールの使用許可をユーザーに求めたとき
- タスクを終了してユーザーの次の入力待ちになったとき
方法
claude code同様、copilot-cliにもhook機能がありますので、
特定のイベントが発生したときにスマホ通知を送るようにします。
claude codeの場合とやることはほとんど同じですが、イベントの種類が少し違うので、そこが解説のポイントです。
スマホはiphoneで試します。
通知の送り方はいくつかありますが、ntfyを使うのが手軽なのでntfyを使います。
copilot-cliにおけるユーザの指示待ち関連イベント
copilot-cliの場合は、以下のフックを設定すればよいです。
- preToolUse
- ツールの使用許可を含め、ユーザに質問する場合は
ask_userというツールが使われるため、このツールが使われた直後に通知を送ればよいです。 - postToolUseは
ask_userに限らずすべてのツール利用で発生するイベントのため、コマンド側でツール名がask_userの場合のみ通知を送るようにする必要があります。 - 試していませんが、
ask_user自体がツールなので、postToolUseでツール名がask_userのときに通知を送る方法でも動く気はします。
- ツールの使用許可を含め、ユーザに質問する場合は
- agentStop
- タスク完了やユーザの次の入力待ちに戻るタイミングで発生するイベントです。
- ややこしい別のイベントとして
sessionEndもありますが、こちらはセッション全体の終了時に発生するイベントなので少し違います。
通知方法
通知の送り方はいくつかありますが、まずはntfyを使うのが手軽だと思います。
無料版だと1日あたりの通知回数に制限があるので、不足を感じたら別の方法を試すと良いと思います。
以下、ntfyを前提の説明となります。
ntfyをスマホアプリでインストールして、ntfyのトピックを決めます。
トピック名はランダムな文字列等を含めて、他者とかぶらないようにすることを推奨します。他者と被ると、知らない人の通知を受け取ったり、自分の通知が知らない人に届くことになります。
copilot-cliのhook設定
copilot-cliのhook設定は、リポジトリルートの.github/hooksにファイルを作成して行います。ユーザ単位で背設定したい場合は~/.copilot/hooksに指定してもよいです。
ここではリポジトリに設定する場合を説明します。
例えば以下のようなファイルを作成します。
{
"version": 1,
"hooks": {
"preToolUse": [
{
"type": "command",
"bash": "./notify.sh",
"cwd": "scripts",
"timeoutSec": 10
}
],
"agentStop": [
{
"type": "command",
"bash": "./notify.sh",
"cwd": "scripts",
"timeoutSec": 10
}
]
}
}
設定内容は以下です。
- 2種類のイベント
preToolUseとagentStopに対してフックを設定 - フックの内容は、bashコマンドを実行するタイプで、
scriptsディレクトリにあるnotify.shを実行するように設定
フックで呼び出すスクリプトの内容
ntfyで通知を送るためのスクリプトの例は以下の通りです。
環境変数COPILOT_NTFY_TOPICにntfyのトピックを指定しておくと、そのトピックに通知が送られるようになります。COPILOT_NTFY_SERVERとCOPILOT_NTFY_PRIORITYはデフォルト値から変えたい場合は指定します。hooksの場合は何もしなくても.bashrc等で指定した環境変数を読めていた気がしますが、もし読めない場合は、スクリプト内で直接指定したり、BASH_ENVを使う方法を試すと良いと思います。
今はpreToolUseとagentStop両方のイベントで同じスクリプトを呼び出すようにしていますが、イベントごとに別のスクリプトを用意しても良いと思います。
#!/usr/bin/env bash
set -u -o pipefail
log_debug() {
echo "[notify_hook] $*" >&2
if [ -n "${COPILOT_NTFY_DEBUG_LOG:-}" ]; then
mkdir -p "$(dirname "$COPILOT_NTFY_DEBUG_LOG")"
printf '[notify_hook] %s\n' "$*" >>"$COPILOT_NTFY_DEBUG_LOG"
fi
}
topic="${COPILOT_NTFY_TOPIC:-}"
if [ -z "$topic" ]; then
log_debug "skip reason=no_topic"
exit 0
fi
if ! command -v jq >/dev/null 2>&1; then
log_debug "skip reason=missing_jq"
echo "jq が見つからないため ntfy.sh 通知をスキップします" >&2
exit 0
fi
if ! command -v curl >/dev/null 2>&1; then
log_debug "skip reason=missing_curl"
echo "curl が見つからないため ntfy.sh 通知をスキップします" >&2
exit 0
fi
input="$(cat)"
server="${COPILOT_NTFY_SERVER:-https://ntfy.sh}"
priority="${COPILOT_NTFY_PRIORITY:-default}"
configured_event_name="${COPILOT_HOOK_EVENT:-}"
if ! cwd="$(printf '%s' "$input" | jq -r '.cwd // ""')" ||
! formatted_timestamp="$(printf '%s' "$input" | jq -r 'if .timestamp == null then empty else ((.timestamp / 1000) + (9 * 60 * 60) | floor | strftime("%Y/%m/%d %H:%M:%S")) end')" ||
! tool_name="$(printf '%s' "$input" | jq -r '.toolName // .tool // empty')" ||
! tool_args="$(printf '%s' "$input" | jq -r '.toolArgs // empty')" ||
! stop_reason="$(printf '%s' "$input" | jq -r '.reason // .stopReason // empty')" ||
! fallback_tool_args="$(printf '%s' "$input" | jq -r '(.args // []) | join(" ")')"; then
log_debug "skip reason=invalid_json"
echo "hook 入力の JSON 解析に失敗したため通知をスキップします" >&2
exit 0
fi
if [ -z "$tool_args" ]; then
tool_args="$fallback_tool_args"
fi
event_name="$configured_event_name"
if [ -z "$event_name" ]; then
if [ -n "$tool_name" ]; then
event_name="preToolUse"
else
event_name="agentStop"
fi
fi
build_pre_tool_use_message() {
local tool_name="$1"
local tool_args="$2"
local question
local choices_str
if [ "$tool_name" != "ask_user" ]; then
log_debug "skip reason=not_ask_user tool=$tool_name"
return 1
fi
title="${COPILOT_NTFY_TITLE:-Copilot CLI: ユーザ確認待ち}"
question="$(printf '%s' "$tool_args" | jq -r '.question // empty' 2>/dev/null || true)"
choices_str="$(printf '%s' "$tool_args" | jq -r '(.choices // []) | join(", ")' 2>/dev/null || true)"
message=""
if [ -n "$question" ]; then
message="question: $question"
fi
if [ -n "$choices_str" ]; then
message="$message
choices: $choices_str"
fi
if [ -z "$message" ] && [ -n "$tool_args" ]; then
message="args: $tool_args"
fi
}
build_agent_stop_message() {
local stop_reason="$1"
title="${COPILOT_NTFY_TITLE:-Copilot CLI: エージェント停止}"
message=""
if [ -n "$stop_reason" ]; then
message="reason: $stop_reason"
fi
}
case "$event_name" in
preToolUse)
if ! build_pre_tool_use_message "$tool_name" "$tool_args"; then
exit 0
fi
;;
agentStop)
build_agent_stop_message "$stop_reason"
;;
*)
log_debug "skip reason=unsupported_event event=$event_name"
exit 0
;;
esac
message="$message
cwd: $cwd"
if [ -n "$formatted_timestamp" ]; then
message="$message
timestamp: $formatted_timestamp"
fi
log_debug "event=$event_name topic=$topic cwd=$cwd"
if ! curl \
--silent \
--show-error \
--fail \
-X POST \
-H "Title: $title" \
-H "Priority: $priority" \
--data-binary "$message" \
"${server%/}/$topic" >/dev/null; then
log_debug "send=failed event=$event_name topic=$topic"
echo "ntfy.sh への通知に失敗しました" >&2
else
log_debug "send=ok event=$event_name topic=$topic"
fi
exit 0
動作確認
上記の設定とスクリプトを用意した状態でcopilot-cliを起動して、ユーザの指示待ちになるようなタスク(例えば「フックのテストをしたいのでユーザになにか許可を求めてください」)を投げてみてください。ユーザの指示待ちになるタイミングでスマホに通知が届けば成功です。
プレミアムリクエストがもったいないのでgpt-4.1で試すと良いと思います。
おわりに
copilot-cliで指示待ちのタイミングでスマホ通知する方法について解説しました。
いろいろ改良点はありそうですが、とりあえずスマホに通知は届くようになったので満足しています。
どなたかのご参考になれば。