目的
- SlackにPRレビュー依頼 → 自動返信の仕組みを追加コストなしで実現
- Claudecode proプランの定額内でやりたい
できること
- Slackチャンネルに GitHub の PR の URL を投稿する
- Bot が Claude Code(
claude -p)を呼び出して PR をレビューする - レビュー結果を、Markdownのテキストスニペットとしてスレッドに返す
メリット
- 最小限コスト(ClaudeCode pro プラン と ローカルPCのみ)で回せる
- Claude Console の API(別料金)を使わないで済む
デメリット
- ローカルPC(でbot.py)が動いてないと動作しない
構成
Slack(チャンネルに PR の URL を投稿)
↓ Socket Mode
bot.py(Mac 上で常駐、Python + Slack Bolt)
↓ subprocess
claude -p(gh pr view / gh pr diff で PR を読んでレビュー)
↓
Slack のスレッドにスニペットで返信
前提
- Mac(ターミナルで作業)※Windowsでもいけると思う
- Python 3
- Claude Code がインストール済みで、Pro / Max プランにログインできる
- GitHub CLI(
gh)がインストール済み - Slack ワークスペースにアプリを追加できる権限
注意:
claude -pはサブスクリプションの利用枠を消費する。チーム全体で使う場合は、Claude Console の API キーを使うほうが適している(コスト次第)。
1. Slack アプリを作る
- https://api.slack.com/apps を開き、Create New App → From a manifest を選ぶ。
- ワークスペースを選び、次の JSON を貼り付けて作成する。
{
"display_information": { "name": "PrReviewBot" },
"features": {
"bot_user": { "display_name": "PrReviewBot", "always_online": true }
},
"oauth_config": {
"scopes": {
"bot": ["chat:write", "channels:history", "groups:history", "files:write"]
}
},
"settings": {
"event_subscriptions": {
"bot_events": ["message.channels", "message.groups"]
},
"org_deploy_enabled": false,
"socket_mode_enabled": true,
"is_hosted": false,
"token_rotation_enabled": false
}
}
| スコープ | 用途 |
|---|---|
chat:write |
メッセージを送る |
channels:history / groups:history
|
公開/プライベートチャンネルのメッセージを読む |
files:write |
レビュー結果をスニペットで送る |
2. トークンを2つ取得する
| トークン | 取得場所 | 先頭の文字 |
|---|---|---|
| App-Level Token |
Basic Information → App-Level Tokens → Generate Token and Scopes(スコープは connections:write) |
xapp- |
| Bot User OAuth Token | Install App → Install to Workspace → 表示されたトークン | xoxb- |
後からスコープを追加したときは、OAuth & Permissions 画面上部の reinstall your app を押してインストールし直す。
3. チャンネルに Bot を追加
- チャンネル名をクリック → エージェントとアプリ → アプリを追加する → PrReviewBot を追加
同じ画面の 概要 タブの一番下にある チャンネルID(C0 で始まる文字列)を控えておく。
4. Claude Code と GitHub CLI の準備
# Claude Code 用の長期トークンを発行(sk-ant-oat01-... が表示される)
claude setup-token
# gh がログイン済みか確認(未ログインなら gh auth login)
gh auth status
5. 作業フォルダと Python 環境を作る
mkdir -p ~/pr-review-bot && cd ~/pr-review-bot
python3 -m venv venv
source venv/bin/activate
pip install -U slack_bolt slack_sdk
6. .env を作る
cat > .env <<'EOF'
export SLACK_BOT_TOKEN=xoxb-*********
export SLACK_APP_TOKEN=xapp-*********
export CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-*********
EOF
chmod 600 .env
.envは Git にコミットしないように(.gitignoreに追加)。
7. bot.py を作る
TARGET_CHANNEL を、3. で控えたチャンネルIDに書き換える。
# pip install slack_bolt
import os, re, subprocess, json, time, threading
from datetime import datetime
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
app = App(token=os.environ["SLACK_BOT_TOKEN"])
TARGET_CHANNEL = "C0*********" # 対象チャンネルのID
PR_RE = re.compile(r"https://github\.com/([\w.-]+/[\w.-]+)/pull/(\d+)")
PROMPT = """GitHubのPR {repo}#{num} をレビューしてください。
`gh pr view {num} --repo {repo}` と `gh pr diff {num} --repo {repo}` で内容を確認し、
機能・品質・セキュリティ・データ整合性・パフォーマンスの観点で、
日本語で簡潔に指摘してください。結論(Approve可/要修正)を最後に書いてください。"""
# ターミナルに時刻付きで経過を表示する
def log(msg):
print(f"[{datetime.now():%H:%M:%S}] {msg}", flush=True)
@app.event("message")
def on_message(event, say, client):
# 受け取ったメッセージをすべて表示する(どこで止まっているか調べるため)
log(f"メッセージ受信:channel={event.get('channel')} bot_id={event.get('bot_id')} text={event.get('text', '')[:60]}")
if event.get("channel") != TARGET_CHANNEL or event.get("bot_id"):
return
m = PR_RE.search(event.get("text", ""))
if not m:
return
repo, num = m.group(1), m.group(2)
ts = event["ts"]
log(f"PRを検出:{repo}#{num}")
say(text="レビューを開始します…", thread_ts=ts)
# Claude Code を stream-json で起動し、動きを1行ずつ表示する
start = time.time()
proc = subprocess.Popen(
["claude", "-p", PROMPT.format(repo=repo, num=num),
"--allowedTools", "Bash(gh pr view:*),Bash(gh pr diff:*)",
"--output-format", "stream-json", "--verbose"],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
timer = threading.Timer(900, proc.kill) # 15分で強制終了
timer.start()
log("Claude Code を起動しました")
review = ""
error = ""
for line in proc.stdout:
try:
ev = json.loads(line)
except json.JSONDecodeError:
log(f" {line.rstrip()[:200]}") # JSON以外(エラー文など)はそのまま表示
error += line
continue
if ev.get("type") == "assistant":
for c in ev["message"].get("content", []):
if c.get("type") == "tool_use":
cmd = c.get("input", {}).get("command", "")
log(f" 実行中:{c.get('name')} {cmd}")
elif c.get("type") == "text" and c.get("text", "").strip():
log(f" 考え中:{c['text'].strip()[:80]}…")
elif ev.get("type") == "result":
review = (ev.get("result") or "").strip()
if ev.get("is_error"):
error += review
review = ""
proc.wait()
timer.cancel()
elapsed = int(time.time() - start)
log(f"Claude Code 終了({elapsed}秒, 終了コード {proc.returncode})")
if not review:
say(text=f"エラー:{error[:500] or 'タイムアウトまたは出力なし'}", thread_ts=ts)
log("エラーをSlackに返信しました")
return
# Markdown のテキストスニペットとしてスレッドに送る
client.files_upload_v2(
channel=TARGET_CHANNEL,
thread_ts=ts,
content=review,
filename=f"review-{repo.replace('/', '-')}-{num}.md",
title=f"PRレビュー {repo}#{num}",
snippet_type="markdown",
initial_comment=f"レビューが終わりました({elapsed}秒)",
)
log("レビュー結果をSlackに送信しました")
if __name__ == "__main__":
log("Botを起動します")
SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start()
ポイント
-
--allowedTools:gh pr viewとgh pr diffだけを許可しているので、許可を求める確認で止まらず、それ以外のコマンドも実行されません。 -
--output-format stream-json --verbose:Claude Code の動きが JSON で1行ずつ流れてくるので、実行中のコマンドをターミナルに表示できる。最後の"type": "result"の行のresultにレビュー本文が入っている。 -
files_upload_v2(snippet_type="markdown"):長いレビューも、文字数制限で切れずにスニペットとして全文届きく。
8. 起動する
cd ~/pr-review-bot
source venv/bin/activate
source .env
python bot.py
[10:24:52] Botを起動します
⚡️ Bolt app is running!
と表示されれば起動できている。ターミナルを閉じると Bot も止まる。
9. 動作確認
チャンネルに PR の URL を投稿すると、ターミナルにこう表示される。
[10:25:30] メッセージ受信:channel=C0********* bot_id=None text=https://github.com/...
[10:25:30] PRを検出:owner/repo#123
[10:25:31] Claude Code を起動しました
[10:25:40] 実行中:Bash gh pr view 123 --repo owner/repo
[10:25:48] 実行中:Bash gh pr diff 123 --repo owner/repo
[10:27:10] Claude Code 終了(99秒, 終了コード 0)
[10:27:11] レビュー結果をSlackに送信しました
トラブルシューティング
| 症状 | 原因と対処 |
|---|---|
| ターミナルに何も出ない | Bot がチャンネルに入っていない、またはイベント設定の誤り |
メッセージ受信 の channel が違う |
TARGET_CHANNEL を表示された ID に直す |
メッセージ受信 だけで止まる |
URL が https://github.com/…/pull/数字 の形になっていない |
Claude Code を起動しました で止まる |
CLAUDE_CODE_OAUTH_TOKEN を確認する |
gh のところでエラー |
gh auth status でログインとリポジトリの権限を確認する |
missing_scope エラー |
files:write を追加して、アプリを再インストールする |