はじめに
Claude Code に大きめのリファクタリングを任せて席を外した。しばらくしてキッチンから戻ってくると、スピーカーから声が聞こえてきた。
「ボス、テストが全部通ったのだ。」
ずんだもんだった。
これは Claude Code の hook という機能を使って組んだ完了通知の仕組みだ。構成はシンプルで、「Claude Code が作業を終えたら Haiku が transcript を要約し、VOICEVOX のずんだもんが声で読み上げる」というだけ。全部シェルスクリプト 150 行で動く。
ターミナルを覗きに行かなくて済む、という話ではない。作業の完了が音として空間に届くという体験が根本的に違う。デスクトップ通知は目が向いていなければ気付けないが、声は席を外していても耳が拾ってくれる。
この記事では、その仕組みの作り方と、設計の中で面白かった部分を書く。
全体像:AI が AI を使って AI に喋らせる
まず全体の流れを見てほしい。
Claude Code に指示する
│
└─ UserPromptSubmit hook 発火
│
└─ VOICEVOX コンテナが起動する(ずんだもんが目覚める)
Claude が作業する(人間は席を外す)
│
└─ Stop hook 発火
│
├─ Haiku が transcript を要約(AI #2)
│
└─ ずんだもんが要約を読み上げる(AI #3)
Claude Code が作業し(AI #1)、Haiku が要約し(AI #2)、VOICEVOX のずんだもんが話す(AI #3)。三段のパイプラインが全部シェルスクリプトで繋がっている。
そして、話しかけたらずんだもんが起きて、報告が終わったら眠る。この「ライフサイクルの連動」が一番面白い部分で、後のセクションで詳しく説明する。
セットアップ
リポジトリを公開しているので、クローンするだけで使える。
git clone https://github.com/ryokwkm/claude-task-reporter.git ~/.claude/hooks/claude-task-reporter
~/.claude/settings.json に 2 つの hook を登録する。
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/claude-task-reporter/voicevox_sync.sh",
"timeout": 10
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/claude-task-reporter/session_summary.sh",
"timeout": 60
}
]
}
]
}
}
設定ファイルを作成する。
cp ~/.claude/hooks/claude-task-reporter/session_summary.conf.example \
~/.claude/hooks/claude-task-reporter/session_summary.conf
VOICEVOX を使う場合、初回は Docker イメージ(約 2〜3GB)のダウンロードが走る。2 回目以降は数秒で起動する。
bin/ を PATH に追加しておくと、ON/OFF の操作が楽になる。
export PATH="$HOME/.claude/hooks/claude-task-reporter/bin:$PATH"
| コマンド | 説明 |
|---|---|
csconfig |
ON/OFF をトグル |
csstatus |
現在の状態を表示 |
csedit |
設定ファイルを vi で開く |
一番の工夫:話しかけた瞬間にずんだもんが起動する
VOICEVOX は Docker コンテナで動かす。常時起動でもいいが、使っていないときにメモリを食い続けるのは無駄だ。
そこで Claude Code に話しかけた瞬間にコンテナを起動し、通知を無効化したら止めるという設計にした。
Claude Code には UserPromptSubmit という hook がある。ユーザーがプロンプトを送信した瞬間に任意のコマンドを実行できる。これと作業完了時の Stop hook を組み合わせると、こういう世界が作れる。
| hook | タイミング | やること |
|---|---|---|
UserPromptSubmit |
話しかけた瞬間 | VOICEVOX コンテナを起動(必要なら) |
Stop |
Claude が応答し終わった瞬間 | transcript を要約してずんだもんに喋らせる |
実装は voicevox_sync.sh。「設定ファイルの CS_ENABLED の値」と「コンテナの実際の稼働状態」を見比べて、ズレていたら補正するだけのシンプルな設計だ。
if [ "${CS_ENABLED:-false}" = "true" ]; then
if ! is_running; then
start_container # docker run
fi
else
if is_running; then
stop_container # docker stop
fi
fi
ただし、UserPromptSubmit hook はユーザーの入力をブロックする。コンテナ起動に数秒かかるので、そのまま書くとプロンプトを送るたびに待たされてしまう。
解決策は self-fork。親プロセスを即座に終了させ、実処理を子プロセスに引き継ぐ。
# 親プロセスは即座に exit、子で処理を継続
if [ -z "${VOICEVOX_SYNC_FORKED:-}" ]; then
VOICEVOX_SYNC_FORKED=1 nohup "$0" "$@" >/tmp/voicevox_sync.log 2>&1 &
exit 0
fi
これで hook の応答は数ミリ秒で返り、コンテナ起動はバックグラウンドで進む。Claude Code に話しかけながら、裏でずんだもんが静かに目を覚ます。
ずんだもんに喋らせる
VOICEVOX はローカルで動く音声合成エンジンで、HTTP API を持っている。curl で音声データ(WAV)を生成して afplay で再生するだけで動く。
speak_with_zundamon() {
local text="$1"
local wav="/tmp/claude_zunda_$$.wav"
# ① テキストから音声クエリを生成
local query
query=$(curl -s -X POST "${VOICEVOX_URL}/audio_query?speaker=${VOICEVOX_SPEAKER}" \
--get --data-urlencode "text=${text}") || return 1
# ② クエリから WAV を合成
curl -s -X POST "${VOICEVOX_URL}/synthesis?speaker=${VOICEVOX_SPEAKER}" \
-H "Content-Type: application/json" \
-d "$query" -o "$wav" || return 1
# ③ 再生
afplay "$wav"
rm -f "$wav"
}
ステップは 3 つだけ。/audio_query でテキストをパラメータ化し、/synthesis で WAV に変換し、afplay で鳴らす。特別なライブラリもランタイムも不要で、シェルスクリプトの範囲で完結する。
フォールバックの設計
VOICEVOX は Docker が動いていないと使えない。そのため CS_VOICE_MODE で 3 段階の動作を設定できるようにしてある。
| 値 | 動作 |
|---|---|
auto(デフォルト) |
VOICEVOX が動いていればずんだもん、止まっていれば say
|
say |
常に macOS 標準の say
|
zundamon |
常にずんだもん(起動していなければ読み上げをスキップ) |
デフォルトの auto は、Docker が入っていない環境や、コンテナが何らかの理由で起動に失敗した場合でも say で報告してくれる。道具が壊れても動き続ける、という小さな安心感がある。
プロンプトで「聴かれること」を前提にする
Stop hook で transcript の末尾 30 行を Haiku に渡して要約させるのだが、このプロンプトに一工夫した。
通常、LLM へのプロンプトは「これを読む人間」を前提に書く。しかし今回の出力は誰も読まない。そのまま音声合成エンジンに渡され、スピーカーで流れる。
プロンプトの冒頭にこう書いた。
「あなたが生成する文章は、人間に読まれることはなく、そのまま音声合成エンジン VOICEVOX(声: ずんだもん)に渡されて、ボスのスピーカーで読み上げられます。誰も途中で校正しません。したがって、求められているのは『文字として正しい文章』ではなく『耳で聞いたときに自然で聞き取りやすい話し言葉』です。」
読者は人間ではなく音声合成エンジンだ、と明示する。
これを書く前後で出力の自然さが大きく変わった。マークダウンの記号が混入しなくなり、英単語の読み方(haiku → 「ハイク」)も自然に変換されるようになった。文字数も「80〜100文字、15秒以内」と数値で指定することで、ずんだもんの読み上げ速度とテンポが合うようになった。
LLM への指示は「誰が・どんな文脈で受け取るか」を書くだけで品質が変わる。今回の場合、受け取るのが人間ではなく音声合成エンジンだというのは、書いてみて初めて気付いた視点だった。
おわりに
Claude Code の hook は「LLM が応答を返したタイミングで任意のシェルスクリプトを実行する」というシンプルな仕組みだ。
今回作ってみて面白かったのは、hook を 1 本ではなく 2 本組み合わせると「ライフサイクル」を定義できるということだった。UserPromptSubmit(会話の開始)と Stop(会話の終了)のペアで、「Claude と話している間だけ動くインフラ」が作れる。今回は VOICEVOX コンテナを対象にしたが、同じ発想でデータベースを起こしたり、監視ツールを動かしたりといった応用も考えられる。
Claude Code に複雑な作業を任せながら席を外せるようになった。ずんだもんが「ボス、〇〇が完了したのだ」と声をかけてくれる環境は、慣れると手放せなくなる。
クレジット: 本記事の音声読み上げ機能は [VOICEVOX:ずんだもん] を利用しています。