Claude codeが処理終了したタイミングを通知してくれるフックを作った

注意: これは私の出身地、 Native Win32 コーディングの話が多量に含まれるので、 web 出身の人はてきとーに話半分で聞いてください。

最近、ついにclaude codeを2つ立ち上げて別のものを同時に作るみたいなことをやり始めまして。まだ頭が付いてこない感もあるのですが、まぁこういうことに慣れて置いた方が、なにかと将来の役に立つかもしれませんし。

で、それをやり始めたのはいいんですが、結局人間のほうが無理で並列度がなかなか上がりません。どうにかならんもんかと思ってちょっと考えました。

私の場合、一番ボトルネックになっているのが、裏で走っている方のclaude codeがいつ処理終了したのかわからないということでした。定期的にウインドウを切り替えて確かめないとわからないから、そこで無駄な行き来が発生する感じです。そろそろ終わったかなと思ってウインドウ切り替えてみるけど、まだやってるじゃないかおいとなって戻ってくるみたいなことをわりと繰り返す必要があります。もしかしたら、画面が見えている人からして見れば、ウインドウを2つ並べて両方見ればええやんとかそういう解決策があるのかもしれませんが、私は1個しか見られないので、どうしてもここで差を付けられてしまうのかと勝手に妄想しました。

丁度、 claude code にフックという機能が実装されたときに、これを使えば処理が終わって入力待ちになったタイミングとかとれるようになるんかなと想像していたのですが、今こそそのツールを作る時じゃね?と思い立ちました。で、作りました。 100% claude code だけで。

claude code のフックってなんやねん

あらかじめ規定されているチェックポイントで、 claude code に外部のコマンドをたたかせることができます。

公式の説明 に一覧がありますが、まぁこんな感じで、ある程度の制約はつきますが、任意のタイミングで処理を挟み込めるわけです。

たしか、ユーザーの入力待ちになるみたいなフックがあったような記憶がおぼろげにありました。これでコマンドをたたかせて、「処理が終わったよ」みたいなことをしゃべってくれれば、どのウインドウにいてもすぐに気づいて戻ってこられるんじゃないかと思いました。しかも、 SAPI 5 でしゃべらせれば、スクリーンリーダーと別口にできるので、操作している途中にしゃべっても次の操作でかき消してしまうみたいなこともないなと考えていました。

とはいえ作るのはめんどかった

全盲エンジニアが、エンジニアとして使えるレベルで Mac を手懐けるのは難しいです。これは VoiceOver のバグによるところが大きいです。なので、ホストマシンは原則 Windows となります。

基本的には、しゃべる機能は、ホストマシンである Windows の上で動かさないといけません。音声読み上げで SAPI 5 などの Windows COM をたたくことを考えたり、コンパクトサイズの standalone exe を生成することを考えると、おとなしく MSVC++ で開発したいところです。

私は元々 MSVC++ 出身みたいなところがあるので、まぁ書こうと思えば書けますが、さすがにあの CoInitialize から始まる一連のあれを暗記してなにも見ずに書けます!なんてことはありません。多少の調べは必要です。あと、 cmd parse したり json parse したり、 UTF-16LE を扱ったり、本質的でない面倒な作業が多すぎます。つまり、手で書くのはめんどい。

ちょっとまてよ。面倒なら全部 claude code に書かせればいいじゃないか。

結論、30分ぐらいでできました

yncat/claudecode-notifier: Notify claude code background sessions waiting for user input

まずは書記プロンプト

こういうテキストファイルを作りました。ちなみにリポジトリにもコミットしてあります。

# 背景
claude code で複数セッションを扱っているときに、裏のウインドウで claude code がユーザー入力待ちになっても気づくことができません。
claude code には、ユーザーの入力待ちになったときに、特定の実行ファイルを呼び出せるフックという機能があるそうなので、これで「ユーザー入力待ちになったタイミング」を通知するコマンドラインアプリケーションを作成します。

# 開発環境
Windows専用でOK
Microsoft Visual C++ を利用

# 動作
claude code のフックに登録することを想定
ユーザーの入力待ちになったフック情報を受け取り、「○○のセッションが入力待ちになりました」というような通知をしゃべる
しゃべるために、 Windows の SAPI5 機能を利用する

# 進め方
1. claude code のフックについて web で情報収集を行い、そのレポートを claude_code_hook.md に書き込んでください。
2. claude_code_hook.txt を読んだ上で、実装計画を立ててください。それを design_doc.md に書き込んでください。
3. visual studio projectを作成してください。作業中のCLIは、visual studio developer command prompt なので、vsのコマンドは通る前提でかまいません。
4. design_doc.md を読み、実装をしてください。
5. プロジェクトをビルドするための makefile を作成してください。
6. makefile でプロジェクトがビルドできることを確認してください。
7. 英語版のreadme.mdを作成してください。
8. 日本語版のreadme_ja.mdを作成してください。内容は英語版と同じにしてください。
9. 英語版と日本語版に相互リンクを貼ってください。

claude code をキックする

md と txt を間違えたりしていますが、まぁ細かい事はいいんです。どうせ claude 先生がいい感じにしてくれます。

今回は msvc++ のツールチェーンを読み込みたいので、 visual studio の開発者用コマンドプロンプトを立ち上げます。普通の cmd だと途中で失敗します。あとは、 claude code を立ち上げて、「claude code のフック用のアプリケーションを作成したいです。 prompt.txt に進め方を書きました。ファイルを読み、全てのタスクを実施してください」と打ち込んでエンターを押し、へいへいといろんなコマンドを許可していくだけです。

1分ぐらいでウェブ調査をし、30秒ぐらいで design doc を下記、また1分ぐらいで実装をし、 makefile でビルドをしようとして何回か失敗し、なんかを直し(早すぎてなにを直したか見えなかった)、完成しちゃいました。最初のバージョンが。早すぎる。

中身を見てみる

驚くべきことに、 SAPI5 でしゃべる部分は1発で完璧な実装ができていました。イメージをつかんでもらうために初期化処理のところだけ引用しますが、なかなか複雑なんです。

    HRESULT hr = CoInitialize(nullptr);
    if (FAILED(hr)) {
        std::cerr << "Failed to initialize COM" << std::endl;
        return false;
    }

    hr = CoCreateInstance(CLSID_SpVoice, nullptr, CLSCTX_ALL, IID_ISpVoice, (void**)&pVoice);
    if (FAILED(hr)) {
        std::cerr << "Failed to create SAPI voice instance" << std::endl;
        CoUninitialize();
        return false;
    }

これを人間が書くのはなかなか大変なので、これが一瞬でできたことは単純にすごすぎます。

しかも、勝手にテストコードを書き、勝手にそれを実行してテストまでやっていました。 nmake test を実行したいというので、まあいいんじゃねと思って許可したらいきなりしゃべり初めてすげえとなりました。

が、ちょっとおいおいというところもありました。なんと、文字列検索で json parse を簡易的に実装していました。こりゃああかん。

json parse に計量ライブラリを使わせる

私がよく使っている header-only の json parse ライブラリ があるので、これを使ってもらうことにしました。 header をプロジェクトファイルに突っ込んで、「 picojson っていうライブラリを使ってね。 src/picojson.h にいれといた」みたいなことを言ったら、1分ぐらいで実装してくれました。しかもなんか1発で動いてました。やばい。

しかも、そのあとにライセンス表記とかも全部生成させたけど完璧に書いてくれました。

この状態で実はもう動きました

はい。この段階で普通に exe が生成されてたので、それを普通にフックに登録して使ってみたら、普通に使えました。普通にって3回も言っちゃった。

使ってみたけどなんか遅いなあ

notification という種類のフックを使っていました。別に私はそんな指示一言もしてないんですけどね。説明を見てみたら、ユーザーの入力待ちになってから60秒経ったら通知を送ると書いてました。なんとかこの60秒さえも節約できないものか。

マニュアルを眺めてたら、

Runs when the main Claude Code agent has finished responding.

と書いてる “stop” というフックがありました。こっちのほうが早いんじゃないか。

ということで、「 stop 」というフックがあるらしいからそれにも対応してと打ち込んでエンターを押し、ごろごろしていたら、勝手になんかをリファクタリングし、対応してくれました。

と、ここまでやったものを私はハッピーに使っているだけです!今回はreadmeすら自分で書いてません!

次の日に release workflow とかを作ってもらいました。これも x86 と x64 の両方のビルドをリリースしたいと言ったら、いい感じにやってくれました。 github actions のジョブ設計ぐらいはしたんですけどね。まぁいうてもこのジョブでなにをして、このジョブでなにをして、2つそろったら両方のアーティファクトから x86 / 64 ビルドをとってリリースしてねって感じのことを書き下しただけです。

結論

もう claude code がない生活には戻れないです。しかも、入力待ちになったら自動でしゃべるようになったので、より拘束に AI をたたけるようになっちゃいました。

ちなみに、 wsl2 からも Windows バイナリをたたけるので、普通にシンボリックリンクして wsl2 の claude code から使うことができます。控えめに言って最高です。