はじめに:レビューは自動化できたのに、対応は手作業のままだった
当社(合同会社ジョインクラス)は、社内業務の約98%を launchd の定期ジョブと Claude Code の無人実行で回しています。PR にも AI レビューを入れていますが、しばらくは指摘への対応だけ人間がやるという状態でした。
「このnullチェック抜けてます」「この変数名わかりにくいです」程度の指摘に、毎回ブランチをチェックアウトして直して push する。これは自動化できるはずです。
ただし、何も考えずに「レビューコメントが来たら Claude に直させる」を作ると、次の3つの事故が起きます。
- 無限ループ:AIが修正 push → AIレビューが再実行 → また指摘 → また修正…
-
破壊:AIが勝手に
git push --forceしたり、関係ないファイルを書き換える - コスト爆発:1回の呼び出しが想定より高く、ループと掛け算になる
筆者は 2 を実際にやらかしています。ローカルの無人実行で AI が git push --force を実行し、ブランチが消えました。この記事は、その反省を踏まえて3つの事故をワークフローの構造で防ぐ設計を、そのまま使える YAML とスクリプトで紹介します。
全体設計
[AIレビュアー] --review comment--> [GitHub]
│ pull_request_review / pull_request_review_comment
▼
[ai-review-fix.yml]
├─ ガード1: 自分のbotの発言は無視(ループ防止)
├─ ガード2: 対応回数ラベルで上限3回(ループ防止)
├─ ガード3: fork PRは対象外(権限防止)
├─ claude -p(allowedTools制限 + max-budget)
├─ ガード4: 変更ファイルの許可リスト検査
└─ 通常pushのみ(force禁止)+ 結果をPRにコメント
ポイントは「AIにお願いして守らせる」のではなく、AIが何を出力しても事故にならない位置にガードを置くことです。プロンプトでの禁止は補助にすぎません。
手順1:ベースにした自社ラッパー
CI版を書く前に、当社で毎日動いている無人実行ラッパーを紹介します。CI版はこれをほぼそのまま移植しています。
#!/bin/bash
# claude を無人実行するときの共通ラッパー。実コストを記録する。
_CLAUDE_RUN_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
claude_run() {
local label="$1"; shift
claude --output-format json \
--fallback-model claude-sonnet-5 \
--max-budget-usd 3 \
"$@" 2>/dev/null \
| python3 "$_CLAUDE_RUN_LIB/record-cost.py" "$label"
}
呼び出し側はこうです(記事自動生成ジョブの実コード)。
claude_run replenish --effort low --allowedTools "Write,Read,Edit" -p "..."
ここから得た教訓が3つあります。
-
--max-budget-usdは必ず付ける。実測すると「2+2」を聞くだけでも1回 $0.27〜0.47 かかっていました。内訳の大半はキャッシュ作成の約41,000トークンで、自社の CLAUDE.md やスキルは5,800トークンだけ。残りは固定オーバーヘッドなので、削れるのは呼び出し回数だけです。CIではこれがループ回数の上限設計に直結します。 -
--output-format jsonでコストを取る。標準出力のtotal_cost_usdを記録しておくと、あとで「どのジョブが高いか」がわかります。 -
--bareは使わない。認証を読まずNot logged inで落ちます(筆者はこれで1晩分のジョブを全滅させました)。
手順2:ワークフロー本体
.github/workflows/ai-review-fix.yml を作ります。
name: ai-review-fix
on:
pull_request_review:
types: [submitted]
# 同じPRで並走させない(ループ・競合push防止)
concurrency:
group: ai-review-fix-${{ github.event.pull_request.number }}
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
env:
MAX_ROUNDS: 3
FIXER_BOT: github-actions[bot]
jobs:
fix:
# ガード1: 指摘が付いたレビューのみ / 自分自身のレビューは無視
# ガード3: fork PR は secrets を渡さないので対象外
if: >
github.event.review.state == 'changes_requested' &&
github.event.review.user.login != 'github-actions[bot]' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
# ガード2: 対応回数をラベルで数える
- name: Check round limit
id: round
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
run: |
n=$(gh pr view "$PR" --json labels \
-q '[.labels[].name | select(startswith("ai-fix-round-"))] | length')
echo "count=$n" >> "$GITHUB_OUTPUT"
if [ "$n" -ge "$MAX_ROUNDS" ]; then
gh pr comment "$PR" --body "AI自動対応は ${MAX_ROUNDS} 回で打ち切りました。人間の確認をお願いします。"
exit 1
fi
- name: Collect review comments
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
REVIEW_ID: ${{ github.event.review.id }}
# 本文は env 経由で渡す(run に直接埋め込むとスクリプトインジェクションになる)
REVIEW_BODY: ${{ github.event.review.body }}
run: |
gh api "repos/${{ github.repository }}/pulls/$PR/reviews/$REVIEW_ID/comments" \
--jq '.[] | "- \(.path):\(.line // .original_line) \(.body)"' > /tmp/comments.md
printf '%s\n' "$REVIEW_BODY" >> /tmp/comments.md
cat /tmp/comments.md
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code
- name: Run fixer
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: bash .github/scripts/ai-fix.sh /tmp/comments.md
# ガード4: 触ってよいファイル以外が変わっていたら中止
- name: Verify changed files
run: bash .github/scripts/verify-diff.sh
- name: Test
run: npm ci && npm test
- name: Commit and push (no force)
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
ROUND: ${{ steps.round.outputs.count }}
run: |
git diff --quiet && { echo "no changes"; exit 0; }
git config user.name "$FIXER_BOT"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "fix: AIレビュー指摘への自動対応 (round $((ROUND+1)))"
git push origin HEAD
gh pr edit "$PR" --add-label "ai-fix-round-$((ROUND+1))"
gh pr comment "$PR" --body-file /tmp/fix-summary.md
なぜこれでループしないのか
ループ防止は3重にしています。
| 層 | 仕組み | 止まるケース |
|---|---|---|
| トークン |
GITHUB_TOKEN での push は新しいワークフローを起動しない(GitHubの仕様) |
修正pushでAIレビューが自動再実行されない |
| 発言者 | review.user.login != 'github-actions[bot]' |
自分のコメントに反応しない |
| 回数 |
ai-fix-round-N ラベルで上限3回 |
AIレビュアーが別トークンで毎回指摘しても3回で止まる |
1層目だけで足りそうに見えますが、AIレビュアーを PAT や GitHub App で動かしていると修正 push を拾って再レビューします。回数上限はどの構成でも効く最後の砦なので必ず入れてください。上限に達したら失敗させて人間に通知する、が正解です。
手順3:修正スクリプト(claude -p 部分)
#!/bin/bash
set -euo pipefail
COMMENTS_FILE="$1"
RESULT=$(claude -p "$(cat <<EOF
あなたはPRのレビュー指摘に対応するエンジニアです。
以下の指摘のうち、妥当なものだけを最小差分で修正してください。
ルール:
- 指摘された箇所以外を変更しない
- 指摘が誤りだと判断したら修正せず、理由を書く
- git操作は一切しない(コミット・pushはCIが行う)
最後に「対応した指摘 / 対応しなかった指摘と理由」をMarkdownで出力すること。
## レビュー指摘
$(cat "$COMMENTS_FILE")
EOF
)" \
--output-format json \
--allowedTools "Read,Edit,Grep,Glob" \
--max-budget-usd 1 \
--max-turns 20)
echo "$RESULT" | jq -r '.result' > /tmp/fix-summary.md
echo "cost: $(echo "$RESULT" | jq -r '.total_cost_usd') USD"
設計上のポイント:
-
--allowedToolsからBashとWriteを外す。Editは既存ファイルの書き換えのみなので、新規ファイル生成も git 操作も物理的にできません。git push --forceの事故は「禁止する」ではなく「Bash を渡さない」で防ぎます。 - 「指摘が誤りなら直さない」を明示する。AIレビューは誤検知もします。従わせるだけだと、正しいコードを壊して指摘に合わせる挙動になります。対応しなかった理由がPRコメントに残るので、人間はそこだけ見ればよくなります。
-
--max-budget-usd 1× 上限3回 = 1PRあたり最大 $3。上限が計算できることが大事です。
手順4:変更ファイルの検査
AIの出力を信用せず、結果の差分で判定します。
#!/bin/bash
set -euo pipefail
# 触ってはいけないパス
DENY='^(\.github/|package-lock\.json|.*\.env|migrations/)'
# 1回の自動対応で許す最大変更行数
MAX_LINES=200
changed=$(git diff --name-only)
if echo "$changed" | grep -Eq "$DENY"; then
echo "::error::保護対象ファイルが変更されました"
echo "$changed" | grep -E "$DENY"
git checkout -- .
exit 1
fi
lines=$(git diff --numstat | awk '{s+=$1+$2} END {print s+0}')
if [ "$lines" -gt "$MAX_LINES" ]; then
echo "::error::変更が${lines}行あり上限${MAX_LINES}行を超えました"
git checkout -- .
exit 1
fi
echo "OK: ${lines} lines changed"
.github/ を保護しているのは、AIが自分のワークフローを書き換えてガードを外せないようにするためです。地味ですが一番大事な行です。
落とし穴
実装時にハマりやすい点をまとめます。
-
pull_request_review_commentで起動すると、コメント1件ごとにジョブが走る。レビューで5件指摘されると5並列で起動します。pull_request_reviewのsubmittedで受けて、そのレビューのコメントをまとめて取得するのが正解です。 -
concurrencyのcancel-in-progress: trueにしない。修正途中のジョブがキャンセルされ、中途半端な状態で次が走ります。 -
fork PR を対象にしない。
pull_request_targetに変えれば secrets は使えますが、第三者のコードを書き込み権限付きで実行することになります。社外コントリビュータのPRは人間が対応しましょう。 - テストを AI 修正の後に必ず走らせる。テストが落ちたら push しないので、壊れたコードがPRに積まれません。
検証方法
導入したら、わざと指摘を出すPRで以下を確認してください。
# 1. テスト用ブランチで明らかな指摘が出るコードを入れる
git switch -c test/ai-fix
echo 'export const f = (x) => x.length' > src/tmp.ts
git add . && git commit -m "test: ai-fix" && git push -u origin HEAD
gh pr create --fill
# 2. 手動で changes_requested レビューを付ける(AIレビュアーの代わり)
gh pr review --request-changes -b "src/tmp.ts: x が undefined のときに落ちます"
# 3. ワークフローの実行とラベルを確認
gh run watch
gh pr view --json labels,comments -q '.labels[].name, .comments[-1].body'
確認するのは4点です。
- 修正コミットが1つ積まれ、
ai-fix-round-1ラベルが付く - 修正 push の後にワークフローが再起動しない
- 同じレビューを3回付けると、4回目で「打ち切りました」コメントが出て止まる
-
.github/を変更させるような指摘を出すと、verify-diff で失敗する
特に 2 と 3 はループ防止の確認なので、本番導入前に必ず通してください。
まとめ:自動化は「安心して任せるための仕組み」
当社では以前、AIが書いた営業メールを確認せず自動送信し、取引先に敬語のおかしいメールが届いたことがあります。それ以来、対外アクションはすべて draft → 承認 → 実行 に通しています。
今回のCIも同じ考え方です。AIにレビュー対応を任せますが、マージするかどうかは人間が決める。AIが担当するのは「直す」と「直さなかった理由を書く」までです。
自動化は AI の能力を制限するためのものではなく、安心して任せるための仕組みです。ガードを構造で入れておけば、AIの出力が多少ぶれても事故にはなりません。
もっと深く知りたい方へ
この記事では「レビュー指摘への対応」だけを扱いましたが、要件定義 → 設計 → 実装 → レビュー → 修正の開発ライフサイクル全体を AI 前提で組み直す方法は、拙著『AI駆動開発ライフサイクル(AI-DLC)の教科書』で体系的にまとめています。CI へのレビュー組み込みや、人間が判断すべきポイントの切り分けも扱っています。
書籍一覧はこちら → https://zenn.dev/joinclass?tab=books