2026.02.10
Claude Code の Hooks でプラグインを作って公開するまでの話
Takashi Kakizoe
普段から Claude Code を使って開発することが多いのですが、長めのタスクを投げて別の作業をしていると、「あれ、もう終わってたのか...」と気付くのが遅れることがよくありました。
通知が欲しいな、と思って調べてみると Claude Code には Hooks という仕組みがあり、イベント発生時に任意のコマンドを実行できるようになっています。
最初は .claude/settings.json に say コマンドを 1 行書くだけだったのですが、気付いたらどんどん育ってしまい...最終的にプラグインとして公開するところまでやってみました。
作ったものは cc-notifier-voice として公開しています。
この記事では、Hooks でできることの紹介から、プラグインとしてまとめて公開するまでの流れを書いていきます。
1. Claude Code の Hooks とは
Claude Code の Hooks は、エージェントのライフサイクルの各ポイントでシェルコマンドを実行できる仕組みです。 設定は JSON で書きます。
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "say '入力待ちです'"
}
]
}
]
}
}この例ですと、Claude が入力待ちになるたびに macOS の say コマンドで読み上げてくれます。
コマンドには stdin 経由で JSON データ(メッセージ内容やセッション ID など)が渡されるので、イベントの詳細に応じた処理も可能です。
使えるイベント
現時点で使えるイベントはこのあたりです。
| イベント | タイミング |
|---|---|
Notification | Claude が入力を待っている(ユーザーの操作が必要) |
PermissionRequest | ツール実行の許可を求めている |
Stop | レスポンスが完了した |
PostToolUseFailure | ツールの実行が失敗した |
SessionEnd | セッションが終了した |
SubagentStart | SubAgent(Task ツール)が起動した |
SubagentStop | SubAgent が終了した |
これだけイベントがあると、単純な通知以外にもいろいろできます。 たとえば SubAgent の起動・終了を追跡して「今いくつ SubAgent が動いているか」を管理したり、SubAgent 実行中だけ Stop 通知を抑制する、といった制御もできるようになります。
settings.json に直接書く場合の限界
Hooks は .claude/settings.json にコマンドを書けばすぐ動きます。
ちょっとした通知やログ出力ならそれで十分です。
ただ、やりたいことが増えてくると限界が出てきます。
- 複数イベントに対応したい -- 7 つのイベントそれぞれにコマンドを書くと settings.json がかなり長くなる
- 状態管理が必要 -- SubAgent のカウントなど、イベントをまたいだ状態を持ちたい
- 設定を外出ししたい -- 環境変数は Hook のサブプロセスに渡らない場合がある(後述します)
- 他の人にも使ってもらいたい -- settings.json をコピペしてもらう運用はなかなか厳しい
このあたりが「プラグインにまとめたいな」と思ったきっかけでした。
2. プラグインの構成
Claude Code のプラグインは、所定のディレクトリ構成で hooks.json を配置すれば成立します。
ディレクトリ構成
今回作った cc-notifier-voice の構成はこんな感じです。
plugin/
hooks/
hooks.json # フック定義
scripts/
cc-notifier.sh # メインスクリプト(ディスパッチャ)
lib/
config.sh # 設定ファイル読み込み
i18n.sh # 日英メッセージ
macos.sh # 通知 + TTS
state.sh # SubAgent 状態管理
macos/
CCNotifier.app/ # 通知表示用の Swift アプリ
src/
main.swift # ソース
build.sh # ビルドスクリプトplugin/ ディレクトリがプラグインの本体になります。
この中に hooks/hooks.json を置くと、Claude Code がイベント発生時にコマンドを実行してくれます。
hooks.json
{
"description": "macOS notifications with voice announcements for Claude Code",
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/cc-notifier.sh\" notification"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/cc-notifier.sh\" stop"
}
]
}
]
}
}(実際には 7 イベント分ありますが、パターンは全て同じです。)
ポイントは ${CLAUDE_PLUGIN_ROOT} です。
これはプラグインのインストール先パスに展開される変数で、スクリプトを絶対パスで呼び出せるようになります。コマンドの形式は "スクリプトパス" イベント名 という固定パターンで、シェル演算子(&& や |
など)は使いません。
メインスクリプトの流れ
cc-notifier.sh は全イベントの入口になるディスパッチャで、だいたいこんな流れで動きます。
1. macOS かどうか確認(macOS 以外は即終了)
2. ライブラリ読み込み(config.sh, i18n.sh, state.sh, macos.sh)
3. 設定ファイル読み込み
4. 有効/無効チェック
5. stdin から JSON を読んでメッセージとセッション ID を取得
6. イベントに応じたハンドラを実行ひとつ気をつけているのは、set -e を使わないことです。
Hook スクリプトがエラー終了すると Claude Code 側に影響する可能性があるため、どんな状況でも exit 0 で終了する設計にしています。
3. 設計で工夫したところ
環境変数が届かない問題
最初は .zshrc に環境変数を設定して制御しようとしていました。
ところが、Claude Code の Hook はサブプロセスとして実行されるため、ログインシェルの環境変数が引き継がれないケースがあります。
.zshrc に CC_NOTIFIER_LANG=ja と書いても、Hook のスクリプトからは見えない...という状況です。
解決策として、専用の設定ファイル ~/.config/cc-notifier-voice/config を読む仕組みにしました。
# ~/.config/cc-notifier-voice/config
CC_NOTIFIER_LANG=ja
CC_NOTIFIER_SPEED=250
CC_NOTIFIER_TTS_ENABLED=true設定ファイルの読み込みでは eval を使わず、許可リストに基づく case 文で安全にパースするようにしています。
ここは地味ですが、わりと重要なポイントかなと思います。
SubAgent を考慮した通知制御
Claude Code が Task ツールで SubAgent を起動すると、各 SubAgent の終了時にも Stop イベントが発生します。 素朴に全部通知すると、SubAgent が終わるたびに「応答完了」と読み上げられて、かなりうるさいです...。
そこで SubagentStart / SubagentStop イベントでカウンタを管理して、SubAgent が 1 つでも動いている間は Stop 通知を抑制するようにしました。
状態はファイルベースで管理しています。
同時実行のレースコンディションには mkdir ベースのロックで対応しました(macOS 標準の bash 3.2 では flock が使えないため)。
macOS の通知
macOS でデスクトップ通知(バナー)を出すには、通知権限を持つアプリバンドル(.app)が必要です。
osascript でも通知は出せますが、カスタムサウンドやアイコンの制御ができません。
今回は Swift で小さなアプリ CCNotifier.app を作り、UNUserNotificationCenter で通知を送るようにしました。
引数でタイトル・メッセージ・サウンド名を渡す設計で、バンドルの Resources/ にイベントごとのカスタムサウンドファイルを含めています。
CCNotifier.app/
Contents/
MacOS/CCNotifier # ユニバーサルバイナリ (arm64 + x86_64)
Resources/
info.aiff # 通知音
warning.aiff # 警告音
complete.aiff # 完了音
end.aiff # セッション終了音TTS(音声読み上げ)
macOS の say コマンドをそのまま使っています。
言語設定に応じて Kyoko(日本語)/ Samantha(英語)を自動で選択するようにしました。
ひとつ工夫したのは、通知が連続したときの挙動です。
素朴にやると読み上げが渋滞してしまうので、「最後の通知だけ読む」(last-wins)方式にしました。新しい通知が来たら、前の say プロセスを PID ファイル経由で kill してから新しいものを開始します。
4. プラグインとして公開する
marketplace.json を用意する
プラグインを公開するには、リポジトリのルートに .claude-plugin/marketplace.json を配置します。
{
"name": "TakashiKakizoe1109",
"owner": {
"name": "Takashi Kakizoe"
},
"metadata": {
"description": "Plugins by Takashi Kakizoe",
"homepage": "https://github.com/TakashiKakizoe1109/cc-notifier-voice"
},
"plugins": [
{
"name": "cc-notifier-voice",
"version": "0.1.0",
"source": "./plugin",
"description": "macOS-only: Desktop notifications with voice announcements and Slack integration for Claude Code events",
"category": "productivity",
"license": "MIT",
"tags": [
"macos-only"
],
"keywords": [
"notification",
"voice",
"tts",
"macos",
"slack"
],
"homepage": "https://github.com/TakashiKakizoe1109/cc-notifier-voice",
"repository": "https://github.com/TakashiKakizoe1109/cc-notifier-voice"
}
]
}主要なフィールドはこのあたりです。
name: GitHub ユーザー名またはオーガニゼーション名source: プラグイン本体のディレクトリ(hooks.jsonがある場所の親)category:productivity,development,utilitiesなどtags,keywords: 検索やフィルタ用
GitHub リポジトリを作る
プラグインの配布は GitHub リポジトリ経由で行われるため、公開リポジトリとして push します。
git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/TakashiKakizoe1109/cc-notifier-voice.git
git push -u origin mainインストールテスト
公開前に、まず自分の環境でインストールを試しておきます。 Claude Code 内で以下のコマンドを実行します。
/plugin marketplace add TakashiKakizoe1109/cc-notifier-voice
/plugin install cc-notifier-voice/plugin marketplace add は GitHub リポジトリを marketplace のソースとして登録するコマンドで、owner名/リポジトリ名 を指定します。
その後 /plugin install でプラグイン本体をインストールします。
インストールが完了したら Claude Code を再起動して、実際に通知が動作するか確認します。
バイナリの扱いについて
このプラグインには Swift でビルドした CCNotifier.app が含まれています。
GitHub からインストールするとリポジトリの内容がそのまま展開されるため、ビルド済みバイナリをリポジトリに含める必要がありました。
ビルド環境がない方でもすぐ使えるようにという判断ですが、バイナリをリポジトリに入れることへの抵抗感は正直あります...。 インストール後にビルドスクリプトを自動実行する仕組みがあると嬉しいのですが、このあたりは今後の課題かなと思っています。
5. 開発中にハマったところ
コード署名と通知権限
macOS の通知システムはアプリの署名状態を見ています。
開発中に何度かバイナリを再ビルドして ad-hoc 署名(codesign --force --sign -)し直すと、通知センターの設定がリセットされて通知が届かなくなることがありました。
復旧手順としては、システム設定の通知から CCNotifier を探して「バナー」に再設定します。
それでもダメな場合は usernoted の再起動が必要になったりして、ここはなかなか厄介でした...。
bash 3.2 との付き合い
macOS 付属の bash は 3.2 で、いまだに GPLv2 時代のものです。
[[ =~ ]] の正規表現マッチの挙動が新しい bash と微妙に違っていたり、flock が使えなかったりします。
数値バリデーションは case 文のパターンマッチで代用し、排他制御は mkdir のアトミック性を利用しました。
Homebrew で bash 5 を入れればいい話ではあるのですが、プラグインとして配布する以上、標準環境で動くことを前提にしておきたかったので、ここは bash 3.2 に合わせています。
stdin のブロック
Hook のスクリプトでは cat や read で stdin を読む処理があります。
テスト時に stdin を渡し忘れると入力待ちで永遠にブロックするので、何度かやらかしました。
テストのときはこんな感じで JSON をパイプで渡す必要があります。
echo '{"session_id":"test","message":"hello"}' | \
CLAUDE_PLUGIN_ROOT="$(pwd)/plugin" ./plugin/scripts/cc-notifier.sh notification地味ですが、最初はこれに気付かず「スクリプトが固まった...?」と少し焦りました。
6. まとめ
Claude Code の Hooks は、設定ファイルに 1 行コマンドを書くだけの手軽さから始められます。 やりたいことが増えたらスクリプトに切り出して、さらにプラグインとしてまとめれば他の人にも配布できるようになります。
今回作った cc-notifier-voice では以下のようなことをやっています。
- 7 つの Hook イベントに対応した通知と音声読み上げ
- 専用の設定ファイルで環境変数の制約を回避
- SubAgent の状態管理による通知制御
- macOS ネイティブの通知とカスタムサウンド
- Slack / Webhook 連携
プラグインの公開自体は、.claude-plugin/marketplace.json を書いて GitHub に push し、/plugin marketplace add するだけです。
まだエコシステムが若いのでドキュメントが少ないところもありますが、やること自体はシンプルでした。
Hooks で何か便利なものを作ったら、プラグインにして公開してみると面白いかと思います。
最後まで読んでいただき、ありがとうございました!