ここ数年の生成 AI の進化は凄まじく、多くの場面で通常の人間の能力を超えつつあります。
ソフトウェア開発の現場でも、多くの人が AI にコーディングを任せているのが現状だと思います。
それはコードレビューも然りで、GitHub では Copilot Review が GA しており、AI が書いたコードを AI がレビューする時代が既に到来しています。
Azure DevOps (以降 AZD) にも Copilot Review が Public Preview として導入されていますが、今回はそれとは別に Claude Code を使って AZD 上でコードレビューする方法について解説します。
AZD には、既に Copilot Review がビルトインで提供されていますが、完全従量課金の為、コストが読めず、全社的に利用した場合の月々のコストが気になります。
Claude の場合、サブスクリプションの範囲内で使用量のキャップが設けられている為、使い倒した場合に青天井で請求が発生する事態を防ぐことが可能です。
Claude によるコードレビューを実現する為に、次のような仕組みを構築します。
次のコマンドを実行します。
claude setup-token
このコマンドにより、1 年間有効な OAuth トークンが生成されます。
生成したトークンには、user:inference のスコープが割り当てられます。
Claude の公式ドキュメントにこのスコープの詳細は明記されていませんが、setup-token で取得したトークンは、モデルへの推論リクエストのみ可能という記載があります。
その為、コードを読む、生成する、設定された MCP サーバーのツールを呼び出すといったことが可能です。
一方で、Remote Control セッションの確立はできない為、このトークンで実行中のセッションの途中経過を、PC やスマートフォンなどの外部から覗くといったことはできません。
生成した認証トークンに対して、claude.ai から行える操作は Revoke のみです。
claude.ai では、トークンを発行したアプリケーション、スコープ、発行日時のみが判別できる為、いつどのトークンを発行したかは、自分自身で管理する必要がある点に注意してください。
トークンを Revoke するには、claude.ai の左下のアカウントメニューから Settings を開きます。

Settings の Claude Code を開くと、Authorization tokens に発行済みのトークンが一覧表示されるので、ここから対象のトークンを Revoke します。

今回使用するパイプラインの全体は次の通りです。
parameters:
- name: variableGroups
displayName: リンクする変数グループ(Claude Code のトークンを含むもの)
type: object
default: []
- name: tokenVariable
displayName: Claude Code のトークンを保持する secret 変数名
type: string
default: CLAUDE_CODE_OAUTH_TOKEN
- name: model
type: string
default: claude-opus-5
- name: pullRequestId
displayName: レビュー対象の PR 番号(空なら自動判定)
type: string
default: ''
- name: reviewInstructions
displayName: リポジトリ固有の追加レビュー指示
type: string
default: ''
- name: guidelinesFile
displayName: リポジトリ内のレビュー指針ファイル(あれば自動で読み込む)
type: string
default: .claude/review-guidelines.md
- name: excludePaths
displayName: 差分から除外する追加グロブ(カンマ区切り)
type: string
default: ''
- name: maxDiffBytes
type: number
default: 400000
- name: claudeArgs
displayName: claude への追加引数
type: string
default: ''
- name: timeoutSeconds
type: number
default: 1800
- name: pool
type: object
default:
vmImage: ubuntu-latest
- name: jobTimeoutMinutes
type: number
default: 60
- name: nodeVersion
type: string
default: 22.x
- name: claudeVersion
displayName: Claude Code CLI のバージョン(サプライチェーン対策で固定を推奨)
type: string
default: 2.1.223
- name: additionalEnv
displayName: Claude 実行時の追加環境変数
type: object
default: {}
stages:
- stage: ClaudeReview
displayName: Claude Code Review
jobs:
- job: review
displayName: Claude Code Review
timeoutInMinutes: ${{ parameters.jobTimeoutMinutes }}
pool: ${{ parameters.pool }}
variables:
- ${{ each group in parameters.variableGroups }}:
- group: ${{ group }}
steps:
- checkout: self
path: repo
fetchDepth: 0
persistCredentials: true
- checkout: templates
path: tmpl
- task: NodeTool@0
displayName: Use Node.js ${{ parameters.nodeVersion }}
inputs:
versionSpec: ${{ parameters.nodeVersion }}
# バージョンは固定する。latest のままだとパッケージが侵害された際に
# 検知なく CI に取り込まれ、ここで使うトークンが危険にさらされる。
- bash: npm install -g "@anthropic-ai/claude-code@${{ parameters.claudeVersion }}"
displayName: Install Claude Code CLI
- bash: bash "$(Agent.BuildDirectory)/tmpl/scripts/review.sh"
displayName: Review the pull request
env:
# SYSTEM_ACCESSTOKEN は review.sh が PR コメント投稿に使う。
# claude の子プロセスには渡さない(review.sh 側で env -u している)。
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
REVIEW_REPO: $(Agent.BuildDirectory)/repo
CLAUDE_CODE_OAUTH_TOKEN: $(${{ parameters.tokenVariable }})
CLAUDE_CONFIG_DIR: $(Agent.TempDirectory)/claude-config
DISABLE_AUTOUPDATER: '1'
DISABLE_TELEMETRY: '1'
REVIEW_PR_ID: ${{ parameters.pullRequestId }}
REVIEW_MODEL: ${{ parameters.model }}
REVIEW_INSTRUCTIONS: ${{ parameters.reviewInstructions }}
REVIEW_GUIDELINES_FILE: ${{ parameters.guidelinesFile }}
REVIEW_EXCLUDE_PATHS: ${{ parameters.excludePaths }}
REVIEW_MAX_DIFF_BYTES: ${{ parameters.maxDiffBytes }}
REVIEW_CLAUDE_ARGS: ${{ parameters.claudeArgs }}
REVIEW_TIMEOUT_SECONDS: ${{ parameters.timeoutSeconds }}
${{ each pair in parameters.additionalEnv }}:
${{ pair.key }}: ${{ pair.value }}
- task: PublishPipelineArtifact@1
displayName: Publish diagnostics
condition: always()
continueOnError: true
inputs:
targetPath: $(Agent.TempDirectory)/claude-review
artifact: claude-review-$(System.JobAttempt)
このパイプラインを中央のリポジトリに配置しておき、実際に利用する際は、各リポジトリに次のような薄いパイプラインを作って参照するのが良いと思います。
trigger: none
pr: none
resources:
repositories:
- repository: templates # ← エイリアスは templates 固定
type: git
name: {プロジェクト名}/{リポジトリ名} # <TeamProject>/<Repository>
ref: refs/heads/main
extends:
template: {テンプレート名}@templates
parameters:
variableGroups:
- claude-code # CLAUDE_CODE_OAUTH_TOKEN を持つ変数グループ
# 必要に応じて(すべて省略可)
# reviewInstructions: |
# - 本リポジトリは .NET 10 / ASP.NET Core の API です。
# - Controller から DbContext を直接触っていたら優先度「高」で指摘してください。
# excludePaths: '**/*.Designer.cs,**/Migrations/**'
# model: claude-opus-5
テンプレートの内容について解説します。
テンプレートは、利用するリポジトリ毎に挙動を変えられるよう、パラメーターで設定を受け取るようにしています。
| パラメーター | 既定値 | 説明 |
|---|---|---|
| variableGroups | なし | Claude のトークンを保持する変数グループ |
| tokenVariable | CLAUDE_CODE_OAUTH_TOKEN | トークンを保持するシークレット変数名 |
| model | claude-opus-5 | レビューに使用するモデル |
| pullRequestId | 空 | レビュー対象の PR 番号。空の場合は自動判定する |
| reviewInstructions | 空 | リポジトリ固有の追加レビュー指示 |
| guidelinesFile | .claude/review-guidelines.md | リポジトリ内のレビュー指針ファイル |
| excludePaths | 空 | 差分から除外するファイルのグロブ (カンマ区切り) |
| maxDiffBytes | 400000 | Claude に渡す差分の最大サイズ |
| timeoutSeconds | 1800 | Claude の実行のタイムアウト (秒) |
| claudeVersion | 2.1.223 | インストールする Claude Code のバージョン |
レビュー対象のリポジトリ (self) と、テンプレートを配置したリポジトリ (templates) の 2 つを checkout しています。
レビュー対象のリポジトリは、ターゲットブランチとの差分を取得する為、fetchDepth: 0 で全履歴を取得します。
また、後続の処理でターゲットブランチを fetch する為、persistCredentials: true で認証情報を保持させています。
テンプレート側のリポジトリは、レビュー処理を記述したスクリプトを実行する為に checkout しています。
YAML 内でエイリアス名 templates を直接参照している為、利用側のパイプラインでもエイリアスは templates にする必要があります。
npm から Claude Code をインストールします。
ここでは、バージョンを固定してインストールしている点がポイントです。
latest を指定すると、万が一パッケージが侵害された場合に、侵害されたバージョンが気付かないうちに CI へ取り込まれ、パイプラインで扱うトークンが漏洩する危険があります。
バージョンを上げる際は、リリースノートを確認した上で claudeVersion を更新する運用をおすすめします。
実際のレビュー処理は、テンプレート側のリポジトリに配置した review.sh で行っています。
review.sh がこちらです。
#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# Claude Code review for an Azure DevOps pull request.
#
# PR の差分を集める -> Claude にレビューさせる -> コメントを 1 件投稿する
#
# Inputs (environment)
# SYSTEM_ACCESSTOKEN Azure DevOps 用トークン(PR へのコメント投稿に使用)
# CLAUDE_CODE_OAUTH_TOKEN Claude Code の認証トークン(claude setup-token で取得)
# REVIEW_REPO レビュー対象リポジトリのパス(既定 Build.SourcesDirectory)
# REVIEW_PR_ID レビュー対象 PR(空なら自動判定)
# REVIEW_MODEL 既定 claude-opus-5
# REVIEW_INSTRUCTIONS リポジトリ固有の追加指示(任意)
# REVIEW_GUIDELINES_FILE リポジトリ内の指針ファイル(既定 .claude/review-guidelines.md)
# REVIEW_EXCLUDE_PATHS 差分から除外する追加グロブ(カンマ区切り)
# REVIEW_MAX_DIFF_BYTES 既定 400000
# REVIEW_CLAUDE_ARGS claude への追加引数(任意)
# REVIEW_TIMEOUT_SECONDS 既定 1800
# ---------------------------------------------------------------------------
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO="${REVIEW_REPO:-${BUILD_SOURCESDIRECTORY:-$PWD}}"
OUT="${AGENT_TEMPDIRECTORY:-/tmp}/claude-review"
MARKER='<!-- claude-code-review -->'
MODEL="${REVIEW_MODEL:-claude-opus-5}"
log() { printf '%s\n' "$*"; }
warn() { printf '##vso[task.logissue type=warning]%s\n' "$*"; }
fail() { printf '##vso[task.logissue type=error]%s\n' "$*"; exit 1; }
for cmd in git curl jq claude; do
command -v "$cmd" >/dev/null || fail "'$cmd' が PATH にありません。"
done
[[ -n "${SYSTEM_ACCESSTOKEN:-}" ]] || fail "SYSTEM_ACCESSTOKEN が未設定です。ステップの env に \
SYSTEM_ACCESSTOKEN: \$(System.AccessToken) を追加してください。"
[[ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]] || fail "CLAUDE_CODE_OAUTH_TOKEN が未設定です。\
ローカルで 'claude setup-token' を実行して取得したトークンを、secret 変数として設定してください。"
for v in SYSTEM_COLLECTIONURI SYSTEM_TEAMPROJECTID BUILD_REPOSITORY_ID; do
[[ -n "${!v:-}" ]] || fail "${v} が未設定です。Azure Pipelines のジョブ内で実行してください。"
done
# shellcheck disable=SC2153 # Azure Pipelines が設定する変数(TEAMPROJECT とは別物)
API="${SYSTEM_COLLECTIONURI%/}/${SYSTEM_TEAMPROJECTID}/_apis/git/repositories/${BUILD_REPOSITORY_ID}"
mkdir -p "$OUT"
# api METHOD PATH [BODY_FILE] [QUERY] -> RESP / API_ERR
api() {
local method="$1" path="$2" body="${3:-}" query="${4:-}" raw code
local url="${API}/${path}?api-version=7.1"
[[ -n "$query" ]] && url="${url}&${query}"
local -a opts=(
-sS --max-time 120 -X "$method"
-H "Authorization: Bearer ${SYSTEM_ACCESSTOKEN}"
-H 'Accept: application/json'
-w $'\n%{http_code}'
)
[[ -n "$body" ]] && opts+=(-H 'Content-Type: application/json; charset=utf-8' --data-binary "@${body}")
raw="$(curl "${opts[@]}" "$url" 2>&1)" ||
{ API_ERR="curl の実行に失敗: ${raw}"; return 1; }
raw="${raw//$'\r'/}" # Git Bash 対策: curl -w は CRLF を出す
code="${raw##*$'\n'}"
RESP="${raw%$'\n'*}"
[[ "$code" =~ ^2 ]] || { API_ERR="${method} ${path} -> HTTP ${code}: $(head -c 400 <<<"$RESP")"; return 1; }
}
# --------------------------------------------------------------- 1. 対象 PR
PR_ID="$(tr -d '[:space:]' <<<"${REVIEW_PR_ID:-${SYSTEM_PULLREQUEST_PULLREQUESTID:-}}")"
if [[ -z "$PR_ID" ]]; then
# パイプラインを手動実行した場合: ソースブランチからアクティブな PR を探す
q="searchCriteria.sourceRefName=$(jq -rn --arg s "${BUILD_SOURCEBRANCH:-}" '$s|@uri')&searchCriteria.status=active"
api GET "pullRequests" "" "$q" || fail "PR の検索に失敗しました: ${API_ERR}"
PR_ID="$(jq -r '.value[0].pullRequestId // empty' <<<"$RESP" | tr -d '\r')"
[[ -n "$PR_ID" ]] || fail "ソースブランチ ${BUILD_SOURCEBRANCH:-} に対応するアクティブな PR が \
見つかりません。PR を作成するか REVIEW_PR_ID を指定してください。"
fi
api GET "pullRequests/${PR_ID}" || fail "PR ${PR_ID} を取得できません: ${API_ERR}"
jq '.' <<<"$RESP" >"${OUT}/pr.json"
TARGET_REF="$(jq -r '.targetRefName // ""' "${OUT}/pr.json" | tr -d '\r')"
BRANCH="${TARGET_REF#refs/heads/}"
log "PR !${PR_ID}: $(jq -r '.title // ""' "${OUT}/pr.json") (-> ${BRANCH})"
# ----------------------------------------------------------------- 2. 差分
git -C "$REPO" fetch --no-tags --prune origin "+refs/heads/${BRANCH}:refs/remotes/origin/${BRANCH}" \
>/dev/null 2>&1 || warn "origin/${BRANCH} の fetch に失敗しました。ローカルの ref で続行します。"
BASE=''
for ref in "origin/${BRANCH}" "$BRANCH"; do
BASE="$(git -C "$REPO" merge-base "$ref" HEAD 2>/dev/null)" && [[ -n "$BASE" ]] && break
BASE=''
done
[[ -n "$BASE" ]] || fail "${BRANCH} との共通の親コミットが見つかりません。\
checkout の fetchDepth: 0 を確認してください。"
EXCLUDES='**/package-lock.json,**/yarn.lock,**/pnpm-lock.yaml,**/*.min.js,**/*.min.css
**/*.map,**/*.snap,**/*.svg,**/*.png,**/*.jpg,**/*.ico,**/*.pdf
**/bin/**,**/obj/**,**/dist/**,**/node_modules/**'
spec=(-- .)
while IFS= read -r p; do [[ -n "$p" ]] && spec+=(":(exclude,glob)${p}"); done < <(
printf '%s\n%s\n' "$EXCLUDES" "${REVIEW_EXCLUDE_PATHS:-}" | tr ',' '\n' | sed 's/^ *//; s/ *$//' | awk 'NF'
)
git -C "$REPO" diff --name-status "$BASE" HEAD "${spec[@]}" >"${OUT}/files.txt"
git -C "$REPO" diff --unified=5 --no-color "$BASE" HEAD "${spec[@]}" >"${OUT}/diff.patch"
if [[ ! -s "${OUT}/diff.patch" ]]; then
log "レビュー対象の変更がありません。"
printf '%s\n## 🤖 Claude Code レビュー結果\n\n✅ **指摘事項なし**\n\nレビュー対象となるコードの変更が検出されませんでした。\n' \
"$MARKER" >"${OUT}/comment.md"
else
MAX="${REVIEW_MAX_DIFF_BYTES:-400000}"
NOTE=''
if [[ "$(wc -c <"${OUT}/diff.patch")" -gt "$MAX" ]]; then
head -c "$MAX" "${OUT}/diff.patch" >"${OUT}/diff.trunc" && mv "${OUT}/diff.trunc" "${OUT}/diff.patch"
NOTE="> 注意: 差分が大きいため先頭 ${MAX} バイトのみ掲載しています。残りは変更ファイル一覧を見て、
> 必要なファイルを Read ツールで直接読んでレビューしてください。"
warn "差分を ${MAX} バイトに切り詰めました。"
fi
# ------------------------------------------------------------ 3. プロンプト
# 動的な部分を先に出し、静的な指示(review-prompt.md)を後ろに連結する。
{
printf '# Pull Request 情報\n\n'
jq -r '"- タイトル: \(.title // "(なし)")",
"- 作成者: \(.createdBy.displayName // "(不明)")",
"- ソースブランチ: \(.sourceRefName // "")",
"- ターゲットブランチ: \(.targetRefName // "")"' "${OUT}/pr.json"
printf '\n## 説明(description)\n\n%s\n\n' \
"$(jq -r 'if (.description // "") == "" then "(説明なし)" else .description end' "${OUT}/pr.json" | tr -d '\r')"
printf '# 変更ファイル一覧\n\n```\n%s\n```\n\n' "$(cat "${OUT}/files.txt")"
printf '# 差分\n\n%s\n\n```diff\n' "$NOTE"
cat "${OUT}/diff.patch"
printf '```\n'
cat "${SCRIPT_DIR}/review-prompt.md"
GUIDE="${REVIEW_GUIDELINES_FILE:-.claude/review-guidelines.md}"
if [[ -n "$GUIDE" && -f "${REPO}/${GUIDE}" ]]; then
printf '\n# このリポジトリ固有のレビュー指針\n\n上記の観点に加えて必ず考慮してください。\n\n'
cat "${REPO}/${GUIDE}"
log "レビュー指針を読み込みました: ${GUIDE}"
fi
if [[ -n "${REVIEW_INSTRUCTIONS:-}" ]]; then
printf '\n# 追加のレビュー指示\n\n%s\n' "${REVIEW_INSTRUCTIONS}"
fi
} >"${OUT}/prompt.md"
# -------------------------------------------------------------- 4. Claude
# 読み取り専用のツールのみ許可する。編集系を許可していないので
# --permission-mode は指定しない(acceptEdits は書き込みの自動承認モードで矛盾する)。
log "Claude を実行します (model=${MODEL}, prompt=$(wc -c <"${OUT}/prompt.md") bytes)"
args=(-p --model "$MODEL" --allowed-tools 'Read,Grep,Glob')
[[ -n "${REVIEW_CLAUDE_ARGS:-}" ]] && { read -r -a extra <<<"${REVIEW_CLAUDE_ARGS}"; args+=("${extra[@]}"); }
runner=(); command -v timeout >/dev/null && runner=(timeout "${REVIEW_TIMEOUT_SECONDS:-1800}s")
set +e
# SYSTEM_ACCESSTOKEN は Claude の子プロセスに渡さない。
# レビュー対象の PR 本文や差分にプロンプトインジェクションが仕込まれていても、
# 環境変数にリポジトリ書き込み権限のトークンが無ければ読み出して漏洩させられない。
# 作業ディレクトリもレビュー対象リポジトリに固定する。
(
cd "$REPO" || exit 127
exec env -u SYSTEM_ACCESSTOKEN "${runner[@]}" claude "${args[@]}"
) <"${OUT}/prompt.md" >"${OUT}/body.md" 2>"${OUT}/claude-stderr.log"
status=$?
set -e
[[ -s "${OUT}/claude-stderr.log" ]] && { log '--- claude stderr ---'; tail -n 40 "${OUT}/claude-stderr.log"; }
[[ "$status" -eq 124 ]] && fail "Claude がタイムアウトしました。REVIEW_TIMEOUT_SECONDS を延ばすか差分を減らしてください。"
[[ "$status" -eq 0 ]] || fail "Claude が異常終了しました (exit ${status})。claude-review アーティファクトを確認してください。"
[[ -s "${OUT}/body.md" ]] || fail "Claude の出力が空でした。claude-review アーティファクトを確認してください。"
{ printf '%s\n' "$MARKER"; cat "${OUT}/body.md"; } >"${OUT}/comment.md"
fi
# 実行情報のフッター
BUILD_LINK="${BUILD_BUILDNUMBER:-${BUILD_BUILDID:--}}"
[[ -n "${BUILD_BUILDID:-}" && -n "${SYSTEM_TEAMPROJECT:-}" ]] &&
BUILD_LINK="[${BUILD_BUILDNUMBER:-$BUILD_BUILDID}](${SYSTEM_COLLECTIONURI%/}/$(jq -rn --arg s "$SYSTEM_TEAMPROJECT" '$s|@uri')/_build/results?buildId=${BUILD_BUILDID})"
printf '\n<sub>🤖 Claude Code / model: `%s` / build: %s</sub>\n' "$MODEL" "$BUILD_LINK" >>"${OUT}/comment.md"
# ------------------------------------------------- 5. 前回のコメントを閉じる
if api GET "pullRequests/${PR_ID}/threads"; then
while IFS= read -r tid; do
[[ -n "$tid" ]] || continue
jq -n '{status:"closed"}' >"${OUT}/close.json"
api PATCH "pullRequests/${PR_ID}/threads/${tid}" "${OUT}/close.json" ||
warn "前回のコメント ${tid} を閉じられませんでした: ${API_ERR}"
done < <(jq -r --arg m "$MARKER" '
(.value // [])[] | select(.isDeleted != true)
| select(((.status // "active") | tostring) == "active")
| select(((.comments // [])[0].content // "") | contains($m)) | .id' <<<"$RESP" | tr -d '\r')
else
warn "既存コメントの一覧取得に失敗しました: ${API_ERR}"
fi
# ------------------------------------------------------------- 6. 投稿する
# 指摘も説明への要望も無ければ解決済みで投稿し、未解決コメントとして残さない。
# 見出し文言は review-prompt.md 側で固定している。
STATUS=active
if grep -q '指摘事項なし' "${OUT}/comment.md" &&
! grep -q 'PR の説明について' "${OUT}/comment.md"; then
STATUS=closed
fi
jq -n --rawfile body "${OUT}/comment.md" --arg status "$STATUS" \
'{comments:[{parentCommentId:0, commentType:"text", content:$body}], status:$status}' \
>"${OUT}/thread.json"
api POST "pullRequests/${PR_ID}/threads" "${OUT}/thread.json" ||
fail "コメントの投稿に失敗しました: ${API_ERR}"
log "PR !${PR_ID} にレビューコメントを投稿しました (status=${STATUS})。"
review-prompt.md がこちらです。
# 依頼
あなたは経験豊富なシニアソフトウェアエンジニアです。
上の Pull Request をレビューし、**PR に投稿する Markdown コメントの本文**を出力してください。
# レビューの進め方
**必ずこの順序で進めてください。**
1. **最初に「説明(description)」を読み、この PR が「何を」「なぜ」変更しようと
しているのかを把握してください。** 差分を読み始めるのはその後です。
説明が無い・薄い場合も、まず変更ファイル一覧から全体像を推測し、
「作者は何をしようとしているのか」の仮説を立ててから差分に進んでください。
2. 次に差分を読み、1 で把握した意図が正しく実現できているかを確認してください。
- 説明に書かれた目的が、差分によって実際に達成されているか
- 説明に無い変更が混ざっていないか(無関係な変更の混入、スコープの逸脱)
- 目的の達成に必要なのに欠けている変更が無いか(設定・マイグレーション・テストなど)
3. 差分だけでは判断できない箇所は `Read` / `Grep` / `Glob` で関連ファイル
(呼び出し元、型定義、既存のテスト、設定ファイルなど)を必ず確認してください。
差分の見た目だけで推測して指摘しないでください。
# レビュー観点
- 不具合・ロジック誤り(境界値、null / 未定義、型の不一致、条件分岐の抜け)
- 例外処理とエラーハンドリング(握り潰し、リソースリーク、リトライの妥当性)
- セキュリティ(インジェクション、認証・認可の抜け、機密情報の混入、入力検証の不足)
- 性能(N+1、不要なループやアロケーション、同期的な I/O、計算量)
- 並行処理・スレッド安全性(競合状態、デッドロック、共有状態の変更)
- 破壊的変更・後方互換性(公開 API、DB スキーマ、既定値の変更、マイグレーション漏れ)
- テスト(変更に対するテストの不足、意味のないアサーション)
- 可読性・保守性(重複、責務の混在、不適切な命名、デッドコード)
- 既存の実装パターン・規約との整合性
- 説明に書かれた意図と実装の乖離
# 指摘のルール
- **優先度を必ず 高 / 中 / 低 のいずれかに分類してください。**
- **高**: そのままマージすると本番障害・データ不整合・セキュリティ事故・重大な仕様違反に
つながるもの。マージ前に修正が必要。
- **中**: 不具合になりうる、保守性を著しく損なう、必要なテストが無いなど。修正を推奨。
- **低**: 軽微な改善提案・可読性の向上など。対応は任意。
- **この PR で変更された箇所**を対象にしてください。既存の無関係な問題は、変更によって
顕在化する場合のみ理由を添えて指摘してください。
- 自動フォーマッタやリンタで解決する些末な指摘(インデント、import 順など)や、
好みの問題は指摘しないでください。
- 推測で断定しないでください。確証が無い場合は指摘しないか、「低」として
「〜になっていないか確認してください」という形で書いてください。
- 同じ原因の指摘は 1 件にまとめてください。件数を稼ぐ必要はありません。
- **問題が無ければ無理に指摘を作らないでください。**
# PR の説明(description)の評価
説明が、**レビュアーがこの PR を理解し安全にマージ判断をするのに十分か**を判定して
ください。判定は説明の**長さ**ではなく、変更の規模・影響範囲に対して必要な情報が
書かれているかで行ってください。
次のいずれかに当てはまる場合のみ、記載を促すセクションを出力してください。
- 説明が空、または「(説明なし)」となっている
- PR テンプレートの雛形だけが残っており、中身が書かれていない
- 変更の規模・影響範囲に対して説明が明らかに不足している
- 「何を変えたか」しか書かれておらず、**なぜ**必要なのかが分からない
- レビュー・リリースに必要な情報が欠けている(破壊的変更の有無、マイグレーション手順、
動作確認の方法など)
- 説明の内容と実際の差分が食い違っている
十分な場合はこのセクションを**丸ごと省略**してください。typo 修正や依存更新のような
軽微な変更で説明が短いことは問題ありません。書く場合は、この PR に即した具体的な項目を
3〜5 個の箇条書きで挙げてください(「詳細を記載してください」のような汎用文は避ける)。
作者を責める書き方はしないでください。
# 出力形式
**Markdown コメントの本文だけ**を出力してください。前置き(「レビューしました」など)、
後書き、全体を囲むコードフェンスは不要です。
レビュアーが**ざっと眺めて要点をつかめる**ことを最優先にしてください。
だらだらとした説明文を書かず、以下のテンプレートに厳密に従ってください。
## 指摘がある場合
````
## 🤖 Claude Code レビュー結果
| 🔴 高 | 🟠 中 | 🟡 低 |
|:---:|:---:|:---:|
| 2 | 1 | 2 |
認証方式を Cookie から Bearer トークンへ変更する PR です。トークン検証に漏れがあります。
---
### 🔴 高(マージ前に要修正)
**`src/Api/AuthService.cs:42`** — トークン期限を検証していない
> `ValidateToken` が `exp` クレームを見ておらず、期限切れトークンで認証を通過できます。
> **修正案:** `TokenValidationParameters.ValidateLifetime = true` を設定してください。
**`src/Api/AuthService.cs:88`** — 例外を握り潰している
> `catch (Exception) { }` で握り潰しており、認証失敗が検知できません。
> **修正案:**
> ```csharp
> catch (SecurityTokenException ex) { _logger.LogWarning(ex, "..."); throw; }
> ```
### 🟠 中(修正推奨)
**`src/Api/OrderService.cs`** — 変更に対するテストがありません
> 認証フローの分岐が増えていますが、対応するテストが追加されていません。
> **修正案:** 期限切れ・改ざんの 2 ケースを `AuthServiceTests` に追加してください。
### 🟡 低(任意対応)
| 箇所 | 指摘 |
|:---|:---|
| `src/Api/AuthService.cs:120` | 変数名 `x` が実態と合っていない |
| `src/Api/Program.cs:15` | 未使用の `using` が残っている |
````
**厳守事項:**
- **冒頭の件数表は必ず 3 列すべて出力**してください(0 件でも `0` と書く)。
- 件数表の直後に、**変更内容とレビュー結果の要約を 1〜3 文**で書いてください。長くしない。
- 続けて `---` で区切ってから、優先度セクションを **高 → 中 → 低 の順**に並べます。
**該当が 0 件の優先度は、見出しごと省略**してください。
- **高・中** の指摘は 1 件ずつ次の形にします。
- 1 行目: `**\`ファイルパス:行番号\`** — 指摘の要約(30 字程度)`
- 2 行目以降: `>` の引用ブロックで「なぜ問題か」を **1〜2 文**、続けて `**修正案:**`
- 指摘と指摘の間には空行を 1 行入れてください。
- **低** の指摘は上の例のように**表 1 つにまとめ**、1 件 1 行で簡潔に書いてください
(引用ブロックや修正案は不要)。
- 行番号が特定できない場合はファイルパスのみ、PR 全体の話なら箇所を省略してください。
- コードを示すときは引用ブロックの中でコードフェンスを使ってください。
## 指摘が無い場合
件数表は出さず、次の形にしてください。
```
## 🤖 Claude Code レビュー結果
✅ **指摘事項なし**
追加された `div` は既存の実装パターンに沿っており、呼び出し元とテストも確認しました。
```
「指摘事項なし」という文字列は、**指摘が 1 件も無いときだけ**使ってください。
## 説明への要望を書く場合
上記のいずれの場合でも、説明が不十分と判定したときは**末尾**に次を追加してください。
見出しは**この文言のまま**にしてください(パイプラインが検知に使用します)。
```
---
### 📝 PR の説明について
以下の追記をお願いします。
- タイムアウト値を 30s から 120s に変更した理由
- 変更による既存バッチへの影響範囲
```
Claude Code の実行に関わる環境変数として、次のものを設定しています。
CLAUDE_CODE_OAUTH_TOKEN: claude setup-token で取得したトークンCLAUDE_CONFIG_DIR: 設定ファイルの保存先。ジョブ毎の一時ディレクトリにすることで、他のジョブと設定が混ざらないようにしていますDISABLE_AUTOUPDATER: CI 上での自動アップデートを無効化し、固定したバージョンを使い続けるようにしていますDISABLE_TELEMETRY: テレメトリの送信を無効化していますreview.sh は、次の流れで処理を行います。
ビルド検証から実行された場合は、System.PullRequest.PullRequestId から PR の Id を取得します。
パイプラインを手動で実行した場合はこの値が設定されない為、実行したブランチをソースブランチとするアクティブな PR を、REST API で検索しています。
パラメーター pullRequestId を指定した場合は、その PR を優先してレビューします。
PR のターゲットブランチを fetch し、共通の親コミット (merge-base) からの差分を作成します。
ロックファイルやビルド成果物、画像ファイルなど、レビューする意味の薄いファイルは、既定で差分から除外しています。
リポジトリ固有で除外したいファイルがある場合は、excludePaths で追加できます。
また、差分が大きすぎる場合は maxDiffBytes で指定したサイズで切り詰め、残りは Claude に変更ファイル一覧から必要なファイルを直接読むよう指示しています。
なお、persistCredentials: true により、.git/config には System.AccessToken が保存された状態になります。
このトークンはジョブの終了と共に失効する為、本記事では削除処理を省略していますが、より厳密に対策する場合は、fetch 後に認証情報を削除してから Claude を実行してください。
プロンプトは、次の順番で連結しています。
review-prompt.md).claude/review-guidelines.md)reviewInstructions)PR の説明や差分といった、PR 作成者が自由に書ける内容を先に置き、静的な指示を後ろに置くことで、差分などに紛れ込んだ指示よりも、本来のレビュー指示が優先されやすいようにしています。
組み立てたプロンプトを標準入力から渡し、claude -p のヘッドレスモードで実行します。
--allowed-tools で使用できるツールを Read、Grep、Glob の読み取り系のみに制限している為、Claude がファイルを書き換えたり、任意のコマンドを実行したりすることはありません。
また、PR へのコメント投稿に使う SYSTEM_ACCESSTOKEN は、env -u で Claude の実行時の環境変数から除外しています。
このトークンはリポジトリへの書き込み権限を持つ為、万が一プロンプトインジェクションが成功した場合でも、Claude から参照できないようにしておくのが安全です。
実行が timeoutSeconds を超えた場合は、タイムアウトとしてパイプラインを失敗させます。
PR が更新される度にレビューが実行されると、古いレビューコメントが溜まっていきます。
その為、投稿するコメントには <!-- claude-code-review --> という HTML コメントを目印として埋め込んでおき、次回の実行時に、この目印を含むアクティブなスレッドを Closed にしています。
Claude の出力に、モデル名とビルドへのリンクをフッターとして付け、PR にスレッドとして投稿します。
指摘事項が無く、PR の説明への要望も無い場合は、スレッドを Closed の状態で投稿します。 未解決のコメントとして残らない為、ブランチポリシーで「コメントの解決」を必須にしている場合でも、マージの妨げになりません。
Claude に渡したプロンプトや実行ログなどを、Pipeline Artifact として保存しています。
レビュー結果が期待通りでなかった場合に、Claude がどのような判断をしたのかを後から確認する為のものです。
condition: always() を指定している為、レビュー処理が失敗した場合でも保存されます。
利用側のパイプラインでは、resources.repositories でテンプレートを配置したリポジトリを参照し、extends でテンプレートを呼び出しています。
PR でのパイプライン実行はブランチポリシーから行う為、trigger と pr はどちらも none にしています。
リポジトリ固有のルールがある場合は、reviewInstructions に追記することで、レビュー観点を追加できます。
より詳細な指針がある場合は、リポジトリ内の .claude/review-guidelines.md に記述しておくと、自動で読み込まれます。
取得したトークンは、YAML に直接書かず、変数グループにシークレットとして登録します。
+ Variable group から claude-code という名前の変数グループを作成しますCLAUDE_CODE_OAUTH_TOKEN という名前で変数を追加し、値に取得したトークンを貼り付けますこのパイプラインは、PR へのコメント投稿と、テンプレートリポジトリの checkout を行う為、それぞれ権限を付与する必要があります。
<プロジェクト名> Build Service (<組織名>) を選択しますContribute to pull requests を Allow に変更しますなお、Project Settings > Pipelines > Settings の Limit job authorization scope to current project for non-release pipelines が無効になっている場合、パイプラインは Project Collection Build Service (<組織名>) として実行される為、こちらに権限を付与してください。
Project Settings > Pipelines > Settings の Protect access to repositories in YAML pipelines が有効な場合、パイプラインの初回実行時に、テンプレートリポジトリへのアクセス許可を求められます。
実行画面に表示される Permit をクリックして許可してください。
作成したパイプラインが PR 作成時に実行されるよう、ブランチポリシーのビルド検証 (Build validation) に登録します。
main) を選択します+ をクリックし、利用側のパイプラインを選択します| 項目 | 設定値 | 説明 |
|---|---|---|
| Trigger | Automatic / Manual | PR の作成・更新時に自動で実行するか、手動で実行するか |
| Policy requirement | Optional | Required にすると、パイプラインが失敗した場合に PR をマージできなくなる |
| Build expiration | Immediately / Never など | ターゲットブランチが更新された際に、再実行を必要とするか |
| Display name | Claude Review | PR 画面に表示される名前 |
AI のレビューはあくまで参考意見として扱う為、Policy requirement は Optional にしておくことをおすすめします。
Trigger を Automatic にした場合、PR の作成時と、ソースブランチへの push 時に自動でパイプラインが実行されます。
Manual にした場合は、PR 画面の Checks から Queue をクリックすることで、任意のタイミングで実行できます。
push の度にレビューが走るとサブスクリプションの使用量を消費しやすくなる為、レビューを依頼したいタイミングで手動実行する運用も検討してください。
パイプラインの実行が完了すると、PR にレビューコメントが投稿されます。
このパイプラインは、ブランチポリシーのビルド検証から実行された際に設定される、System.PullRequest.PullRequestId を使って PR の Id を取得しています。
この値は、PR が作成されたリポジトリと、パイプラインの YAML が配置されたリポジトリが同一でないと取得できません。
その為、テンプレート自体は中央のリポジトリに集約しつつ、レビューを導入したいリポジトリには、それぞれ利用側のパイプラインを配置する必要があります。
AI を利用したコードレビューは、人間よりも高速にリポジトリを横断的にチェックすることができます。
その為、実装箇所毎の方針のずれなど、人間が見逃しがちな部分の指摘漏れを減らすことが期待できます。
一方で、重箱の隅を突くような指摘や、実装経緯を知らないが故の頓珍漢な指摘なども出てくることが多々あり、まだ人間のハンドリングも必要な段階であると筆者は考えています。
ですが、それは人間のレビューにも言えることなので、AI のレビューも人間のレビューも等しく参考にし、レビューをする人手が増えたといった受け取り方で良いのかなと思います。
お問い合わせはこちらから
問い合わせる