ci(claude-review): run reviews on fork PRs via pull_request_target#5104
Conversation
|
|
Reviews (1): Last reviewed commit: "ci(claude-review): run reviews on fork P..." | Re-trigger Greptile |
There was a problem hiding this comment.
Pull request overview
Adds a safe, label-gated pull_request_target path so Claude Code Review can run on fork PRs (where pull_request runs don’t receive secrets/OIDC), while keeping the existing same-repo pull_request behavior unchanged.
Changes:
- Introduces a
pull_request_target(label) trigger for fork PR reviews gated by the newfable-reviewlabel, and consumes the label after triggering. - Hardens fork reviews by keeping the trusted base branch at repo root and exposing the PR head in a separate
./pr-headdirectory for read-only inspection. - Adjusts post-steps to resolve the review bot identity depending on event type (fork vs same-repo).
There was a problem hiding this comment.
This adds a label-gated pull_request_target path so Claude review runs on fork PRs, and the security model is sound: the trusted base branch stays at the workspace root, the contributor head is checked out to ./pr-head pinned to the labeled SHA and added via --add-dir, and the prompt tells the reviewer to treat that tree as untrusted input. I verified the label-consumption step, the pinned-head checkout, the per-event bot-identity resolution in the post-steps, and the empty-github_token preservation of the OIDC path from the diff. One guard change should land before merge.
- Please wrap each disjunct of the job
ifin parentheses in.github/workflows/claude-code-review.yml. The guard mixes&&and||and relies on operator precedence for the gate that keeps fork authors from triggering a secret-bearing run, while the siblingchangeset-semver-check.yaml:214-216parenthesizes exactly this kind of mix and the fork path's runtime behavior cannot be confirmed from the diff alone. - While editing that guard, please decide whether the fork disjunct should keep the
draft == falseandsender.type != 'Bot'checks the same-repo path keeps, and say so in theifcomment; as written, a draft or bot-labeled fork PR is reviewed where a same-repo one is not.
Two smaller notes: the description does not mention that the job permission was widened from issues: read to issues: write (.github/workflows/claude-code-review.yml:85), which also affects same-repo runs, so please note why that is needed; and id-token: write is granted on the fork path although the OIDC exchange is skipped there (github_token is passed directly), so it is unused on that path.
Review threads
- confirmed: issue-comment @changeset-bot[bot] 2026-07-22T10:04:14Z by @changeset-bot[bot]: No changeset is present, which is correct for this CI-only change touching no published package; the bot's own message says you are good to go if no version bump is intended. Not blocking, and the live Semver Check passed.
Review by Rupic. Verified against the diff; runtime behavior was not executed.
Fork PRs were skipped entirely: pull_request runs from forks get no secrets and the OIDC exchange rejects them. Run fork PRs on pull_request_target instead, handing GITHUB_TOKEN to the action directly; those reviews post as github-actions[bot]. The trusted base branch stays at the workspace root so Claude Code never loads CLAUDE.md or .claude/ settings from contributor code, with the PR head exposed read-only at ./pr-head via --add-dir.
a96abc2 to
e846cb6
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
.github/workflows/claude-code-review.yml:107
- For fork PRs, checking out by
ref: ${{ github.event.pull_request.head.sha }}without also settingrepository:will try to fetch that commit from the base repo, which usually doesn't contain the fork's SHA. Setrepositoryto the PR head repo so the pinned SHA checkout works reliably onpull_request_target.
with:
ref: ${{ github.event.pull_request.head.sha }}
path: pr-head
fetch-depth: 1
|
Reviews (2): Last reviewed commit: "fix(ci): harden automatic fork reviews" | Re-trigger Greptile |
There was a problem hiding this comment.
This PR adds a hardened pull_request_target job so Claude review runs on fork PRs, and I recommend merging it. The two jobs are mutually exclusive via event-name and repository-origin guards: claude-review requires github.event_name == 'pull_request' and head.repo.full_name == github.repository, while claude-fork-review requires github.event_name == 'pull_request_target' and a fork head repo. Because adding pull_request_target makes both events fire for same-repo PRs, the new event_name check on the existing job is necessary to prevent a double run, and the check-run state for this PR confirms it: claude-fork-review is skipped here. The same-repository path's steps are otherwise unchanged; only its if guard and the shared REVIEW_PROMPT prose moved, and the prose change drops a dead reference to mcp__github_ci__* tools that were never in the same-repo allowlist. The fork job follows GitHub's pull_request_target hardening guidance: it checks out only the trusted base branch with persist-credentials: false, declares only contents: read and pull-requests: write, passes the built-in github.token, and denies Bash, Read, Write, Glob, Grep, WebFetch, WebSearch, and Agent under dontAsk, leaving read-only GitHub MCP tools plus a single inline-comment write. The trusted post-step publishes the result and updates a workflow-specific marker comment, which also retires the earlier shared-bot-identity concern; every prior review comment (label-gating, ./pr-head, issues: write, id-token: write, guard precedence, secrets.GITHUB_TOKEN) is resolved by this rework. The actionlint, allowlisted-MCP-tool, and action-internal (env scrubbing, credential helper) claims are unverifiable from the diff, as is the fork publish step's runtime success; the same-repo job already PATCHes a PR comment with issues: read + pull-requests: write and no issues: write, and sticky-PR-comment actions post and update PR comments with only pull-requests: write, so it should work and fails loudly if not. Please consider adding issues: read to the fork job's permissions to match the same-repository claude-review job, which declares it for the identical gh api .../issues/{n}/comments read the publish step performs; it is harmless and removes any doubt about the marker-comment lookup.
Review threads
- fixed: review-threads.md .github/workflows/claude-code-review.yml:? (greptile-apps) by @greptile-apps[bot]: The publish step now filters by user.login == github-actions[bot] AND body contains the marker, then PATCHes that specific comment, so it no longer selects the latest generic github-actions[bot] comment.
- fixed: review-threads.md .github/workflows/claude-code-review.yml:? (copilot: head.sha checkout) by @copilot-pull-request-reviewer[bot]: The rework no longer checks out the head SHA; it checks out the trusted base branch with no ref, so the fork-SHA-not-in-base-repo problem is moot.
- fixed: review-threads.md .github/workflows/claude-code-review.yml:? (copilot: empty ref) by @copilot-pull-request-reviewer[bot]: The fork checkout omits ref entirely (only fetch-depth and persist-credentials), so there is no empty ref to misinterpret.
- fixed: review-threads.md .github/workflows/claude-code-review.yml:? (copilot: secrets.GITHUB_TOKEN) by @copilot-pull-request-reviewer[bot]: The fork job uses github_token: ${{ github.token }} and GH_TOKEN: ${{ github.token }}, the built-in token, not secrets.GITHUB_TOKEN.
- stale: prior-reviews.md rupic-app[bot] (parenthesize disjuncts) by @rupic-app[bot]: The current if guard uses only && (no ||), so the operator-precedence concern is moot; the rework removed the disjunctive mix it described.
- fixed: prior-reviews.md rupic-app[bot] (draft/bot checks on fork path) by @rupic-app[bot]: The fork guard keeps github.event.pull_request.draft == false and github.event.sender.type != 'Bot', matching the same-repo path.
- stale: prior-reviews.md rupic-app[bot] (issues: write widening) by @rupic-app[bot]: The rework removed the issues: write widening; the fork job declares no issues permission at all.
- fixed: prior-reviews.md rupic-app[bot] (id-token: write unused on fork path) by @rupic-app[bot]: The fork job no longer grants id-token: write; it passes github.token directly, so OIDC is not requested.
- confirmed: issue-comments.md changeset-bot 2026-07-22T10:04:14Z by @changeset-bot[bot]: Correct for a CI-only change touching no published package; the bot's own message says you are good to go if no version bump is intended. Not blocking.
Review by Rupic. Verified against the diff; runtime behavior was not executed.
Problem
Claude review never runs on external contributors' PRs such as #5085. Fork
pull_requestruns do not receive repository secrets or write tokens, while the Claude action's GitHub App exchange does not supportpull_request_target.Solution
Keep the existing same-repository path unchanged (
pull_request, Claude App token,claude[bot]) and add a separate automatic fork path (pull_request_target, short-lived workflow token,github-actions[bot]). Event and repository-origin guards make the two paths mutually exclusive.Fork security boundary
The fork job follows GitHub's
pull_request_targethardening guidance and the Claude action's non-write-user guidance:contents: readandpull-requests: write; it receives no OIDC or Actions-log permission.track_progress: falsekeeps the action in agent mode. Shell, filesystem, web, editing, and subagent tools are explicitly denied withdontAskmode.allowed_non_write_users: '*'enables automatic external reviews and activates the action's subprocess environment scrubbing and token-free Git credential helper.The accepted residual risks are external users consuming Claude quota and prompt injection influencing review content on their own PR. The workflow does not intentionally expose a path to secrets, command execution, code changes, pushes, or broader repository writes.
Verification
actionlintand its shellcheck integration pass (apart from the repository's existing custom Blacksmith runner-label notice when not configured locally).git diff --checkpasses. This workflow-only change does not need a changeset.Once merged, the next push or reopen on an external PR will trigger the automatic hardened review path.