はじめに
作ったきっかけ
自分はシングルスレッドな人間で、マルチタスクが大の苦手です。
- Slack の返信に、業務の時間のかなりの部分を取られている
- メンションがたくさん来ると、あとで返そうと思ったものを忘れてしまう
そこで、「返し忘れを拾う」「返信の下書きを先に作っておく」「レビューや仕様の確認は決まった時間にまとめて済ませておく」を Claude Code に任せることにしました。自分は、決まった時間に結果を見て、直して送るだけにしています。
やっていること
Mac の launchd から Claude Code をヘッドレス(claude -p)で定時実行し、次のジョブを自動で回しています。
| ジョブ | いつ | やること | 書き込み先 |
|---|---|---|---|
| PR レビュー | 平日 9:00 | レビュー待ち PR をマルチエージェントでレビューし、Slack で決まった仕様とのずれも見る | Slack のレビュー用チャンネル・ローカルの md |
| 仕様ずれチェック | 毎週月曜 10:00 | 1 週間分の Slack の仕様の話と、仕様書・main の実装を突き合わせる。main に入った差分のセキュリティレビューも | 自分用チャンネル |
| 未返信の拾い出し | 毎日 8:45 | 直近 3 日の自分宛てメンション・DM のうち、まだ返していない依頼・質問を一覧にする | 自分用チャンネル |
| Slack 返信の下書き | 平日 10〜19 時の毎正時 | 自分宛てのメンション・DM の返信例を、元のスレッドに下書きとして置く | 元のスレッドの下書き・自分用チャンネル |
Claude Code のクラウド定期実行(routine)を使わず launchd にしたのは、状態ファイル・Slack の Webhook・自作スキル・gh の認証・macOS の通知と、手元の Mac にあるものに頼っているためです。
全体の構成
launchd(時刻で起動)
└─ zsh -lc "claude -p '/<スキル名> を実行して' --allowedTools ..."
└─ Claude Code が ~/.claude/skills/<スキル名>/SKILL.md の手順を実行
├─ gh(GitHub の読み取りだけ)
├─ Slack MCP(読み取り・投稿)
└─ ローカルの状態ファイル(重複を防ぐ)
やることは Skill(SKILL.md)に書き、launchd 側は「このスキルを実行して」と 1 行頼むだけにしています。手順を変えたいときは SKILL.md を直せば済みます。
1. PR レビュー
launchd の設定
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>-lc</string>
<string>cd ~/src/<repo> && ~/.local/bin/claude -p "/pr-review-loop を実行して" --allowedTools "Bash(gh pr list:*)" "Bash(gh pr view:*)" "Bash(gh pr diff:*)" "Bash(date:*)" "Bash(mkdir:*)" "Bash(terminal-notifier:*)" "Bash(osascript:*)" "Read" "Write" "Grep" "Glob" "mcp__claude_ai_Slack"</string>
</array>
<key>StartCalendarInterval</key>
<array>
<dict><key>Weekday</key><integer>1</integer><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer></dict>
<!-- 火〜金も同じ -->
</array>
--allowedTools で、GitHub は gh pr list / view / diff の読み取りだけを許可しています。gh pr review や gh pr comment は許可していないので、無人で動いても PR に勝手にコメントは付きません。
スキルの手順(要約)
-
gh pr listでオープン PR を取り、draft と approve 済みを除く - 状態ファイル(
state.json、{"PR番号": "レビュー済みの head SHA"})と比べ、前回から head SHA が変わっていない PR は飛ばす - PR ごとにマルチエージェントのレビューコマンドを実行する。使っているのは Claude Code のプラグインのコマンドで、6 つのエージェント(品質・コメント・テスト・握りつぶしたエラー・型設計・簡素化)が並列にレビューし、確信度 80 以上の指摘だけを採る
- 汎用の観点に、プロジェクト固有の観点を上乗せする(API を変えたのに仕様書の差分がない、層の分け方の規約に違反している、など)
- Slack で決まった仕様と照合する(下で説明)
- 結果をローカルの
PR-<番号>-<日付>.mdに全文で保存し、Slack には PR ごとに親メッセージ 1 通と、指摘はそのスレッドに投稿する -
state.jsonを更新する
Slack の決定と照合する
仕様の細かい決定は、仕様書より先に Slack のスレッドで決まることがよくあります。PR がその決定どおりかを、レビューのときに確かめさせています。
- PR のタイトル・本文・ブランチ名から、課題キー・PR 番号・機能を表す語を拾う
- それを手がかりに、開発用のチャンネルに絞って直近 60 日の Slack を検索する
- ヒットしたスレッドを読み、「〜にする」「〜はしない」といった決定だけ抜き出す。後から変わっていれば最新の発言を採る
- 決定と PR の差分を比べ、ずれていそうなものは差分とファイルを読み直して確かめる
- ずれは Slack のリンク付きで出す。どちらが正しいかは決めさせない(判断はプロダクトオーナーに回す)
Slack への投稿は、こういう形にしています。
:mag: **#123 PR タイトル**
@author · 🔴2 🟡3 ⚪1 💬1
<PR URL>
💬 は Slack の決定とのずれの件数です。スレッドには Critical と Important、Slack とのずれだけを 1 行ずつ(file.ts:42 の形で)書き、Advisory は件数とローカルのファイル名だけにしています。理由や修正案はローカルの md にだけ書き、Slack は読み流せる量にしています。
2. 未返信の拾い出し(毎朝)
朝 8:45 に、直近 3 日の自分宛てメンションと DM から「まだ返していないもの」を拾い、一覧を自分用チャンネルに 1 通で送ります。
- Slack MCP の検索で、自分宛てメンションと DM / グループ DM を取る(自分の投稿・bot の投稿は除く)
- 1 件ずつスレッドを読み、次をすべて満たすものだけ残す
- そのメッセージより後に、自分の投稿がない
- 自分のリアクションが付いていない
- 依頼・質問・確認依頼・判断待ちである(挨拶・お礼・共有だけのものは除く)
- 古い順に、要件を 1 行に言い換えて並べる。2 日以上前のものには
⏳を付ける
@自分 Slack 見逃し 10/28(2 件)
• <permalink|#チャンネル名> 送信者(10/27 16:18): ○○をチェックしてほしい
「返したかどうか」を検索の結果だけで決めず、スレッドを読んで判断させているのがポイントです。DM で別に返していたものは対応済みとして外します。ルールに書いていない「スレッドで他の人が対応済み」のものを、理由を添えて外したこともありました。
3. Slack 返信の下書き(毎正時)
- Slack MCP の検索で、直近 24 時間の自分宛てメンションと DM を取る
- スレッドを読み、自分が返信・リアクション済みのもの、挨拶・お礼だけのものを除く
- 1〜3 文の返信例を作る。口調は、同じ会話の自分の過去の投稿に合わせる。事実が分からない箇所は
[要確認: 〜]と残し、推測で埋めない - 返信例を、元のスレッドの入力欄に下書きとして置く(Slack MCP の
slack_send_message_draft) - 自分用チャンネルに「どこに下書きを置いたか」を 1 通で通知する
@自分 Slack 返信の下書き 10:00(2 件) リンク先の入力欄に下書きがあります。直して送ってください
• <permalink|#チャンネル名> 送信者(9:12): 要件を 1 行 📝
• <permalink|DM 相手> 送信者(9:40): 要件を 1 行 📝(要確認あり)
通知のリンクを開くと、入力欄に返信例が入った状態になっています。いつもの入力欄で直して、自分で送信します。送信は Claude にはさせていません。
同じスレッドに下書きが既にあるなどで置けなかったときは、通知に返信例の全文を載せ、手でコピーして送ります。
通知は Slack の Incoming Webhook(bot)から送っています。自分のアカウントで投稿すると、自分へのメンションでも通知が鳴らないためです。
最初は「✅ で自動送信」にしていた
最初の版は、通知に ✅ を付けると、5 分ごとのジョブが元のスレッドに自分の名前で送る仕組みでした。二重送信を防ぐ記録や、手で返信済みなら送らない判定も入れていました。
ただ、返信例を直したいときは「通知のスレッドに直した文を書いてから ✅」と手間がかかります。しかも ✅ が無くても 5 分ごとに Claude Code が丸ごと起動していて、空振りでも 1 回あたり数十万トークン(ほとんどはキャッシュの読み込み)を使っていました。
下書きを元のスレッドに置く形に変えたことで、5 分ごとのジョブは要らなくなり、直すのもいつもの入力欄で済むようになりました。
4. 仕様ずれチェック(毎週月曜 10:00)
PR レビューが「マージ前の PR と Slack」を比べるのに対し、こちらは「マージ後の main と Slack」を比べます。最初は平日毎日にしていましたが、今は週 1 回にしています。
- 開発用のチャンネルの、前回の実行以降の投稿とスレッドを読み、仕様の話だけ抜き出す
- 発言ごとに、仕様書と実装を grep して、それぞれ何と言っているかを確かめる
- ずれの候補(Slack と仕様書、Slack と実装、仕様書と実装)ごとにサブエージェントを 1 体起動し、実ファイルを読んで「本当にずれているか、別の PR で解消済みでないか」を判定させる。実在したものだけ残す
- 前回から main に入った差分を、セキュリティレビュー用のエージェントに見せる。CRITICAL / HIGH の指摘は別のエージェントに実コードで確かめさせ、実在したものだけ残す
- 自分用チャンネルに 1 通で報告する。0 件でも「なし」と送る
@自分 仕様ずれチェック 11/2
*仕様ずれ* なし
*セキュリティ*(`<前回のSHA>..<mainのSHA>`)なし
調べるのは専用に作った worktree(main を detached で checkout)です。普段作業しているリポジトリには触りません。
候補を出すエージェントと、実在を確かめるエージェントを分けているのは、それっぽいだけの指摘を減らすためです。
無人で回すときに気をつけたこと
--allowedTools は最小にする
claude -p は対話できないので、許可されていない操作は実行できずに止まります。逆に言えば、許可した操作しかできません。ジョブごとに必要なコマンドだけを書いています。
claude -p "$prompt" --allowedTools "Bash(date:*)" "Bash($dir/post.sh:*)" "Edit($out)" "${extra[@]}" "mcp__claude_ai_Slack"
ファイルの書き込みも、Edit(<状態ファイルのパス>) のようにファイル単位で絞っています。
状態はファイルに持つ
claude -p は実行ごとに記憶がありません。「どの PR をどの SHA でレビューしたか」「どの通知を送ったか」はファイルに書き、次の実行で読ませています。古い行は 24 時間で消しています。
スリープ明けに備える
launchd の StartCalendarInterval は、Mac がスリープ中に時刻が来ると、起きたときに 1 回だけ実行します(man launchd.plist)。cron と違い、朝 Mac を開いたときに 9:00 の分が走ります。
一方、スリープから戻った直後はネットワークがつながっていないことがあるので、実行スクリプトで 3 回まで再試行しています。
for i in $tries; do
if (cd ~ && $claude -p "$prompt" --allowedTools ...); then
exit 0
fi
(( i < ${#tries} )) && sleep 60
done
launchd は「平日の毎正時」を 1 行で書けない
StartCalendarInterval は cron のような範囲指定ができないので、「平日 10〜19 時の毎正時」は「曜日・時・分」の組(plist の <dict>)を、曜日 5 × 時刻 10 で 50 個並べています。
定時のジョブは Sonnet で動かす
claude -p は、指定しなければ普段使いのモデルで動きます。Slack のジョブは --model sonnet を付けて、軽めのモデルで回すようにしました。
claude -p "$prompt" --model sonnet --allowedTools ...
まとめ
- 手順は Skill に書き、launchd からは
claude -p "/<スキル名> を実行して"と 1 行頼むだけにする - 無人で動かすぶん、
--allowedToolsで書き込み先を絞る。GitHub は読み取りだけにして、結果は Slack に集める - 仕様は Slack で決まることが多いので、PR と main の両方を Slack と突き合わせる。どちらが正しいかは人が決める
- 自分の名前で送るものは、Claude に送らせない。元のスレッドに下書きを置き、人が直して送る。分からない事実は
要確認のまま残す - 始めたばかりなので、使いながら直している。頻繁に起動するジョブは、空振りのコストも見ておく
参考
- Claude Code のヘッドレス実行: https://code.claude.com/docs/en/headless
- Claude Code の Skills: https://code.claude.com/docs/en/skills
-
man launchd.plist(StartCalendarInterval)