Claude PR Review
claude-pr-review@1.0.0 Template Claude reviews the pull request diff and posts one ranked review comment on the PR.
Start from it
flightstack template use claude-pr-review What it does
Sends the pull request's diff to Claude and posts one review comment with bugs, risks and concrete fixes, ranked Critical / Should fix / Nits. Re-running the job edits the same comment instead of piling up new ones, and it can optionally fail the check on critical findings.
Inputs
-
repositorystring required - GitHub repository as owner/name, usually "${{ context.repository }}".
-
branchstring required - The PR's head branch, usually "${{ context.branch }}".
-
baseBranchstring defaultmain - Branch the PR merges into.
-
prNumberstring - PR number. Empty looks up the open PR whose head is
branch. -
guidelinesstring - Project-specific review rules appended to the instructions, e.g. the conventions in your CONTRIBUTING.md.
-
modelstring defaultclaude-opus-5-5 - Claude model id.
-
effortlow | medium | high | xhigh | max defaulthigh - How hard Claude thinks.
highsuits code review. -
maxDiffBytesnumber default600000 - Largest diff to send. A bigger diff is cut at this size, and both Claude and the comment say so.
-
failOnnever | critical defaultnever criticalfails the job when the review reports a critical issue, so the PR shows a red check.
Definition
name: Claude PR Review
description: >-
Have Claude review a pull request's diff and post (or update) one review
comment on the PR.
triggers:
- type: pullRequest
jobs:
review:
name: Claude review
type: plugin/claude-pr-review
with:
repository: "${{ context.repository }}"
branch: "${{ context.branch }}"
baseBranch: main
guidelines: >-
Flutter app. Flag missing loading/error states and any widget that
reads MediaQuery for layout.
plugin:
name: claude-pr-review
displayName: Claude PR Review
description: >-
Sends the pull request diff to Claude and posts one review comment with
bugs, risks and concrete fixes, ranked by severity. Re-running the job
edits the same comment instead of piling up new ones.
category: quality
supportedPlatforms:
- linux
- macos
parameters:
type: object
properties:
repository:
type: string
description: GitHub repository as owner/name, usually "${{ context.repository }}".
branch:
type: string
description: The PR's head branch, usually "${{ context.branch }}".
baseBranch:
type: string
default: main
description: Branch the PR merges into.
prNumber:
type: string
default: ""
description: >-
PR number. Empty looks up the open PR whose head is `branch`.
guidelines:
type: string
default: ""
description: >-
Project-specific review rules appended to the instructions, e.g. the
conventions in your CONTRIBUTING.md.
model:
type: string
default: claude-opus-5-5
description: Claude model id.
effort:
type: string
enum: [low, medium, high, xhigh, max]
default: high
description: How hard Claude thinks. `high` suits code review.
maxDiffBytes:
type: number
default: 600000
description: >-
Largest diff to send. A bigger diff is cut at this size, and both
Claude and the comment say so.
failOn:
type: string
enum: [never, critical]
default: never
description: >-
`critical` fails the job when the review reports a critical issue, so
the PR shows a red check.
required: [repository, branch]
script: |
set -euo pipefail
for tool in curl jq git; do
command -v "$tool" >/dev/null 2>&1 || { echo "$tool is required on this agent." >&2; exit 1; }
done
if [ -z "${ANTHROPIC_API_KEY:-}" ]; then
echo "Set ANTHROPIC_API_KEY as a secret variable for this pipeline." >&2
exit 1
fi
if [ -z "${GITHUB_TOKEN:-}" ]; then
echo "Set GITHUB_TOKEN (a token that can comment on pull requests) as a secret variable." >&2
exit 1
fi
repo="{{repository}}"
branch="{{branch}}"
base="{{baseBranch}}"
gh_api() {
curl -sS --fail-with-body \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" "$@"
}
pr="{{prNumber}}"
if [ -z "$pr" ]; then
owner="${repo%%/*}"
pr="$(gh_api "https://api.github.com/repos/$repo/pulls?state=open&head=$owner:$branch" | jq -r '.[0].number // empty')"
fi
if [ -z "$pr" ]; then
echo "No open pull request for $branch in $repo - nothing to review."
exit 0
fi
echo "reviewing $repo#$pr ($branch -> $base)"
git fetch --quiet --unshallow 2>/dev/null || true
git fetch --quiet origin "$base"
diff_file="$(mktemp)"
git diff --no-color "origin/$base...HEAD" > "$diff_file"
size="$(wc -c < "$diff_file" | tr -d ' ')"
if [ "$size" -eq 0 ]; then
echo "Empty diff against $base - nothing to review."
exit 0
fi
limit={{maxDiffBytes}}
truncated=false
if [ "$size" -gt "$limit" ]; then
truncated=true
head -c "$limit" "$diff_file" > "$diff_file.cut" && mv "$diff_file.cut" "$diff_file"
echo "diff is $size bytes; sending the first $limit"
fi
pr_json="$(gh_api "https://api.github.com/repos/$repo/pulls/$pr")"
title="$(echo "$pr_json" | jq -r '.title')"
body="$(echo "$pr_json" | jq -r '.body // ""')"
system_prompt='You are reviewing a pull request as a senior engineer on the team. Report problems that matter: bugs, regressions, security issues, data loss, race conditions, missing error handling, and changes that contradict the stated intent of the PR. Report everything you find and rank it; do not hold back findings to keep the review short. For each finding give the file and line from the diff, what goes wrong and under what input, and a concrete fix. Skip pure style preferences unless the project guidelines ask for them. Format the review as GitHub Markdown: start with a one-line verdict, then sections "Critical", "Should fix", and "Nits", omitting empty sections. If there are critical findings, include the exact text CRITICAL_FINDINGS on its own line at the very end.'
guidelines="$(cat <<'FS_GUIDELINES'
{{guidelines}}
FS_GUIDELINES
)"
if [ -n "$guidelines" ]; then
system_prompt="$system_prompt
Project guidelines: $guidelines"
fi
note=""
if [ "$truncated" = "true" ]; then
note="Note: the diff was $size bytes and only the first $limit bytes are included below. Say in your verdict that the review covers part of the change."
fi
request="$(jq -n \
--arg model "{{model}}" \
--arg effort "{{effort}}" \
--arg system "$system_prompt" \
--arg title "$title" \
--arg body "$body" \
--arg note "$note" \
--rawfile diff "$diff_file" \
'{
model: $model,
max_tokens: 16000,
stream: false,
fallbacks: "default",
output_config: {effort: $effort},
system: $system,
messages: [{
role: "user",
content: ("<pull_request>\n<title>" + $title + "</title>\n<description>\n" + $body + "\n</description>\n</pull_request>\n\n" + $note + "\n\n<diff>\n" + $diff + "\n</diff>")
}]
}')"
response="$(printf '%s' "$request" | curl -sS --max-time 900 https://api.anthropic.com/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2026-10-21" \
-H "anthropic-beta: server-side-fallback-2026-07-01" \
--data-binary @-)"
if [ "$(echo "$response" | jq -r '.type')" = "error" ]; then
echo "Claude API error: $(echo "$response" | jq -r '.error.type + ": " + .error.message')" >&2
exit 1
fi
stop_reason="$(echo "$response" | jq -r '.stop_reason')"
if [ "$stop_reason" = "refusal" ]; then
echo "Claude declined to review this diff: $(echo "$response" | jq -r '.stop_details.explanation // "no explanation"')" >&2
exit 1
fi
review="$(echo "$response" | jq -r '[.content[] | select(.type == "text") | .text] | join("\n")')"
if [ "$stop_reason" = "max_tokens" ]; then
review="$review
_The review hit the output limit and is incomplete._"
fi
critical=false
if printf '%s' "$review" | grep -q '^CRITICAL_FINDINGS$'; then
critical=true
review="$(printf '%s' "$review" | grep -v '^CRITICAL_FINDINGS$')"
fi
marker="<!-- flightstack:claude-pr-review -->"
comment="$marker
## Claude review
$review
<sub>{{model}} via FlightStack - commit $(git rev-parse --short HEAD)</sub>"
payload="$(jq -n --arg body "$comment" '{body: $body}')"
existing="$(gh_api "https://api.github.com/repos/$repo/issues/$pr/comments?per_page=100" \
| jq -r --arg m "$marker" '[.[] | select(.body | startswith($m))][0].id // empty')"
if [ -n "$existing" ]; then
gh_api -X PATCH "https://api.github.com/repos/$repo/issues/comments/$existing" -d "$payload" > /dev/null
echo "updated review comment $existing"
else
gh_api -X POST "https://api.github.com/repos/$repo/issues/$pr/comments" -d "$payload" > /dev/null
echo "posted review comment"
fi
echo "$review"
fs-output critical "$critical"
fs-output prNumber "$pr"
if [ "{{failOn}}" = "critical" ] && [ "$critical" = "true" ]; then
echo "Claude reported critical findings." >&2
exit 1
fi