Claude Code で PR を作ったら、Antigravity(Gemini)にクロスレビューさせる仕組みを作った
概要
個人開発のアプリ(Rails の API + React の SPA + iOS)で、実装はほぼ Claude Code に任せている。Issue を選んでもらって、実装・テスト・PR・セルフレビュー・マージまでを/loopで回すというのを最近はやってる。
で、レビューももうもはや見ないで作ってみていて、セルフレビューは Claude Code の /code-review でやっていたのだけど、Codexにもみてもらうということをやっていました。ただ最近Codexを解約して AntigravityのArgonを使うようにしたので、GitHub連携で動いていたCodexのレビュー部分ががら空きになってしまっていました。
それで、Antigravity Gemini Argon にレビューを自動でしてもらうにはどうするかね、というのに思いを馳せました。
作ったもの
- Claude Code で
gh pr createを実行すると、PostToolUseフックが動く - フックは裏で
agyを起動して、すぐ返る。Claude Code の作業は止めない agyは PR の HEAD を取り出した作業ディレクトリで、読むだけでレビューする- 結果は
<!-- agy-review -->で始まるコメントとして、PR に付く - Claude Code は指摘に対応したら、
--recheckで「直ったか」を確かめてもらう - 最新のレビューが今の HEAD を対象にしていて、「指摘なし」か「まとめ:マージしてよい」のときだけマージする
全体はひとつのシェルスクリプト(.claude/hooks/agy-pr-review.sh)と、.claude/settings.json のフックの設定でできてます。
フックの設定
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(gh pr create*)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/agy-pr-review.sh",
"timeout": 30
}
]
}
]
}
}
matcher で Bash を、if で gh pr create に絞っている。ただ、後で書くとおり、if だけに頼らない作りにしている。
スクリプトの流れ
1. フックとして呼ばれたとき:PR の番号を取り出して、裏で起動する
フックには、ツールの入力と出力が JSON で渡される。そこからコマンドが本当に gh pr create かを確かめて、出力にある PR の URL から番号を取り出す。
INPUT="$(cat)"
printf '%s' "$INPUT" | jq -e '(.tool_input.command // "") | test("(^|[;&|[:space:]])gh pr create")' >/dev/null || exit 0
PR="$(printf '%s' "$INPUT" | jq -r --arg repo "$REPO" '
[.tool_response.stdout?, .tool_response.output?, (.tool_response|strings)] | map(select(.)) | join("\n")
| [scan("github\\.com/" + $repo + "/pull/([0-9]+)")] | last // empty | .[0]')"
[ -n "$PR" ] || exit 0
nohup "$SCRIPT" --run "$PR" >"$LOG_DIR/pr-$PR.log" 2>&1 &
echo "{\"systemMessage\": \"Antigravity で PR #$PR のクロスレビューを裏で始めたよ\"}"
最初Argonのmiddle、認証系などはhighでレビューを依頼していたのでレビューには5〜10分かかる感じでした。フックの中で待つと Claude Code が止まるので、nohup で切り離してすぐ返すなど工夫をしたようです。
2. 裏で:PR の HEAD を worktree に取り出す
HEAD_REF="$(gh pr view "$PR" -R "$REPO" --json headRefName -q .headRefName)"
git fetch -q origin "pull/$PR/head"
git fetch -q origin "$HEAD_REF"
HEAD_OID="$(git rev-parse FETCH_HEAD)"
WORK_DIR="$(mktemp -d)"
git worktree add -q --detach "$WORK_DIR" "$HEAD_OID"
手元の作業ツリーでレビューさせない理由は2つあって一つは手元のブランチや、作業中の変更に左右されない。PR のコードそのものを読ませられる。もうひとつは.env や master.key みたいな git 管理外の秘密情報が、worktree には入らないようにしていました。
差分・PR の本文・ファイルの一覧は、worktree の中の .agy-review/ にファイルで置く。差分は大きくなりうるから、プロンプトに埋め込まないでファイルで渡す。
3. agy を「読むだけ」で動かす
AGY_SETTINGS='{"permissions":{"deny":["command(*)","write_file(*)"]}}'
REVIEW="$(cd "$WORK_DIR" && "$AGY" -p "$PROMPT" --sandbox --model "$MODEL" --settings "$AGY_SETTINGS" --print-timeout 900s)"
シェルの実行とファイルの書き込みは deny にした。読み取りのツール(read_file など)だけで、レビューさせている。シェルが使えないとファイルを探しにくいから、git ls-files の一覧を渡している。
4. PR にコメントする
printf '%s\n## Antigravity(%s)の%s\n\n対象:%s\n\n%s\n' "$MARKER" "$MODEL" "$TITLE" "$HEAD_OID" "$REVIEW" >"$BODY_FILE"
gh pr comment "$PR" -R "$REPO" --body-file "$BODY_FILE"
先頭のマーカー(<!-- agy-review -->)でレビューのコメントを見分けて、「対象:」でどのコミットを見たかを残す。マージしていいかは、この2つで判断する。
再確認(–recheck)
最初は「レビューして終わり」だったんですが、/loopで回しているという背景があったので、自動でレビューしても問題がない状態としたかったため、Antigravityも「レビューOKだよ」といってもらえる体制が必要でした。そのための仕組みを作りました。
再確認では、次のものを渡す。
- 前回の Antigravity のコメント(
previous-review.md) - そのあとのコメント(Claude Code の「こう直した」「これは直さない、理由は〜」)
- 前回の「対象:」のコミットから今の HEAD までの差分(
since-review.diff)
プロンプトには「対応のコメントをうのみにせず、実際のコードで確かめて」と書いた。最後は必ず「まとめ:マージしてよい」か「まとめ:まだ直すところがある」のどちらかで終わらせる。
思考量の切り替え
Gemini の思考量(low・medium・high)は、PR の中身で変えている。
- 再確認:low(見るところが決まっているから)
- ふだん:medium
- 認証・権限・招待・ルーティング・マイグレーション・
.claude/を触る PR:high
SENSITIVE_PATHS='^(server/app/controllers/(application|sessions|signups|...)_controller\.rb|...|server/db/migrate/|\.claude/)'
if [ "$MODE" = recheck ]; then
MODEL="gemini-4-argon-low"
elif CHANGED_FILES="$(gh pr diff "$PR" -R "$REPO" --name-only)" && grep -qE "$SENSITIVE_PATHS" <<<"$CHANGED_FILES"; then
MODEL="gemini-4-argon-high"
else
MODEL="gemini-4-argon-medium"
fi
やはり何時間も/loopさせているとそこそこのトークンが消えるようで、その日のうちに、使える量(Argon の残り)が10%を切った。なので一旦今は SAVE_ARGON=1 で全部 low にしています。本当に慎重に行きたいところ以外は一旦 low でやりくりしたいなーという気持ちです。
つまずいたこと
--mode plan だと止まる
最初は、書き込みをさせないつもりで --mode plan を付けた。でも、人の承認を待つ状態のまま止まった。外して、--settings の deny で縛るやり方にした。
なお、許可を聞く設定(ask)にすると、人がいないモードでは何も出力されないまま終わる。deny にすると、モデルは「使えない」と理解して、読み取りだけで続けてくれた。
macOS の bash 3 と zsh
macOS の /bin/bash は 3.2 で、連想配列が使えない。思考量の表を連想配列で書いていたところで落ちた。
試しに zsh で動かしたときは、set -- $p が単語に分かれなかった。結局、全部を bash 3 で動く書き方にそろえた。
実行中のスクリプトを書き換えると壊れる
bash は、スクリプトを少しずつ読みながら実行する。裏でレビューが動いている最中にスクリプトを直したら、途中から別の行を読んで壊れた。
全体を { ... exit 0 } で囲んで、最初に全部読み込ませるようにした。
set -euo pipefail
{
# ここに全部書く
exit 0
}
フックの if が効かない環境がある
"if": "Bash(gh pr create*)" で絞っていたのに、ほかのコマンドでもスクリプトが呼ばれることがあった。そうなると、出力に偶然 PR の URL が入っていたときに、関係ない PR をレビューしてしまう。
スクリプトの中でも、jq でコマンドが gh pr create かを確かめるようにした。
pipefail と grep -q
gh pr diff --name-only | grep -qE "$SENSITIVE_PATHS" と書いていた。こうすると、grep -q が最初の一致で終わったあとに gh が SIGPIPE で落ちる。pipefail のせいでパイプ全体が失敗扱いになって、high のはずが medium になっていた。
いったん変数に入れてから、grep に渡すようにした。
全角の「:」で cut が使えない
「対象:<コミット>」から前回のコミットを取り出そうとして、`cut -d':'` と書いた。でも `cut` は全角の区切りを扱えない。
40桁の16進だけを grep -oE '[0-9a-f]{40}' で取り出すようにした。
push の直後は headRefOid が古い
直して push した直後に --recheck すると、gh pr view --json headRefOid が push 前のコミットを返すことがあった。古いコードを見て「直っていない」と言われてしまう。
PR のブランチを git fetch して、FETCH_HEAD を使うようにした。
PR を作ったのに、フックが動かなかった
あるとき、レビューが一度も付かない PR があった。ログもない。
調べたら、gh pr create のあとに続けた余計なコマンドが失敗していた。Bash ツールの結果が終了コード 1 になっていたのが原因。PostToolUse は成功したときだけ動くから、PR はできていたのにフックは呼ばれなかった。
それからは、gh pr create を単独で実行するようにしている。(PostToolUseFailure にもフックを置けば拾える)
Claude Code 側のルール
仕組みだけ作っても、待たずにマージされたら意味がない。だから、AGENTS.md と Issue を回すスキルに、次のルールを書きました。
gh pr createのあと、頼んだあとに増えた<!-- agy-review -->のコメントを待つ(最大15分)- 待つ前に、ログ(
.claude/hooks/logs/pr-<番号>.log)があるか確かめる。ないか、エラーで終わっていたら、待たずに PR にコメントして進む - 指摘に対応したら、必ず
--recheckする。自分の/code-reviewだけで済ませない - マージしていいのは、最新のレビューが今の HEAD を「対象:」にしていて、「指摘なし」か「まとめ:マージしてよい」のときだけ
- 往復は2回まで。残ったら PR にコメントして止まる
やってみてどうだったか
1日回して、Claude Code のセルフレビューでは出なかった指摘を、何度も拾ってくれた。たとえばこういうもの。
- Web のセッションの Cookie の期限が、DB の期限と合っていなかった
- Google の ID トークンの
issを確かめていなかった - Google の
credentialがログに出ていた - アカウント作成の途中の Cookie が、別のパスでは消せていなかった。Rails の
cookies.deleteは、そのリクエストに付いていない Cookie には何もしない - 招待リンクのトークンが、URL のパスに入ったままログと APM に送られていた
反対に、Claude Code のセルフレビューのほうが先に見つけたものもあった。完全に任せるより、2つのモデルで見るほうがいい、というのが今のところの感想。
指摘をうのみにしないのも大事だった。「直さない、理由は〜」と PR に書いて、再確認で「直さない理由に納得」と返ってくることもある。理由を書かせると、Claude Code のほうも雑に流さなくなった。
残っていること
- 思考量を全部 low にしているのは、使える量を節約するための一時的な設定。戻すときは
SAVE_ARGON=0にする - 失敗した Bash の結果にもフックを動かすか(
PostToolUseFailure)は、まだ決めていない