公開日
hook で状態を知らせ、`!restart` で会話を引き継ぐ
Slack からは見えない Claude Code 側の出来事を、hook を通して Slack に知らせる仕組みを作った。別セッションの hook が混ざる、--continue が別の会話を開く、--resume で落ちて戻れなくなるなど、再起動まわりで踏んだ問題と対処を書く。
はじめに
Slack から Claude Code を使っていて困るのは、返事が来ないときに理由が分からないこと。使用量の上限に達したのか、ターミナル側で入力待ちになっているのか、セッションが落ちたのか。手元に行かないと分からなかったんだよな。
Claude Code には hook がある。セッションの開始や終了、応答の失敗などの出来事のたびに、決めたコマンドを実行してくれる。これを使って、Slack からは見えない出来事を Slack に知らせる仕組みを作った。
この記事は、その設計と、!restart で会話を引き継ぐまでに踏んだ問題を書く。設計は AI と壁打ちしながら決めたが、Claude Code 側の挙動は実機で確かめるしかなく、いくつかは実際に起きてから直した。
hook はファイルに 1 行書くだけ
hook として起動されるコマンドは、標準入力の JSON から使う欄だけを取り出し、状態ディレクトリの hooks.jsonl に 1 行追記する。それだけ。標準出力には書かず、常に正常終了する。
ブリッジは 1.5 秒ごとに増えた分を読んで、Slack に出す。hook とブリッジが直接やりとりしないのは、MCP の標準出力が MCP 専用だから。ファイルを挟めば、どちらが先に動いていても壊れない。
知らせるのは次のもの。
- ターミナル側の入力待ち。Slack に中継されない確認や、サブエージェントの質問。画面を確認するボタンを付ける
- Claude が Slack に返事をしないまま応答を終えたとき、入力待ちのまま止まっているとき。返事を待っているときだけ出す
- 応答の失敗。使用量の上限、混雑、認証などの種別を添える
- セッションの開始と終了、会話の圧縮とクリア
黙るべきときも決めてある。Slack に中継中の確認があるときは、入力待ちの知らせを重ねて出さない。
上限で止まったあと自動で再開する設定は、CLI の設定では効かなかった。だから上限は知らせるだけにして、再開の操作は手元で行う。
!restart は /exit を打ち込む
hook で状態が分かるようになると、次は Slack から立て直したくなる。channel のプロトコルには Claude Code にコマンドを送る手段が無いので、ターミナルの入力欄に /exit を打ち込む。前回書いた PowerShell スクリプトの出番。
流れはこうだ。状態ディレクトリに再起動の印を置き、画面が空の入力欄で待っているときだけ /exit と Enter を送る。claude.exe が終わるとブリッジも終わり、起動スクリプトが印を見て起動し直す。
印は終了より前に置く必要があるので先に書く。すると、送れなかった経路で印が残り、後で手元で終了したときに勝手に起動し直す問題が出た(#6)。送れなかったときと強制終了に失敗したときに印を消すようにし、「送らなかったときは印が無い」テストを 4 件足した。
別セッションの hook が混ざる(#9)
同じ状態ディレクトリを使うセッションが 2 つあると、hooks.jsonl には両方の hook が書かれる。ブリッジは絞らずに全部 Slack に出していたので、別のセッションの終了まで自分のボットから知らされた。
起動スクリプトが起動ごとに一意の値を環境変数に入れ、hook はそれを行に記録する。ブリッジは自分と同じ値の行だけを扱う。起動スクリプトを使わずに起動したときは、今までどおり全部扱う。
--continue が別の会話を開く(#10)
最初の !restart は --continue で起動し直していた。--continue はフォルダーで最新の会話を開く。同じフォルダーで VS Code の会話が後から作られていると、そちらを開く。10 月 3 日にマジで起きた。
ブリッジは自分の hook の行から今の会話の ID を覚え、再起動の印に書くようにした。起動スクリプトは、印に ID があれば --resume <ID> でその会話を開く。
--resume で落ちて戻れなくなる
これで終わりではなかったんだよな。起動してから一度もやりとりしていないセッションに !restart すると、--resume で起動し直したが、その会話は保存されていない。claude.exe はすぐ終わり、起動スクリプトごと終了して、Slack から戻せなくなった。
対処は 3 段にした。--resume の前に会話の記録ファイルがあるかを確かめ、無ければ新しい会話で起動する。ID が分からないときも新しい会話にする。それでも --resume の起動が 30 秒以内に失敗で終わったら、新しい会話で 1 回だけ起動し直す。
--continue には戻らない。別の会話を開くより、新しい会話で起動するほうがまし。
起動し直した直後の問題
細かい問題がまだ続いた。
- 旧ブリッジの終了処理と新ブリッジの起動が重なり、ロックが取れずに縮退モードで起動した(#7)。起動時に最大 10 秒ロックを取り直し、持ち主のプロセスが死んでいるロックは取るようにした
- 新ブリッジは起動の 30 秒前までの hook を読み直すので、前のブリッジが知らせ済みの「セッションを開始した」をもう一度投稿した。最後のセッション開始より後の分だけを渡すようにした
!restart forceでclaude.exeだけを止めると、MCP サーバーや Bash の子が残った(#8)。プロセスツリーごと止め、親がclaude.exeかnode.exeでなければ止めない
どれも、実機で !restart を繰り返して見つけた。hook のイベント名や欄は Claude Code の版で変わりうるので、通知が出ないときは hooks.jsonl に何が記録されているかを見る、とドキュメントに書いてある。自分のための注意書き。
このシリーズはここで一区切りにする。experimental の機能に乗っている以上、また壊れるんじゃないか。壊れたら、そのときの記録を書く。