ci: gate claude.yml on same-repo PRs and pin actions to commit SHAs - #1966
Conversation
…1882) Two pieces of hardening on the Claude Code workflow, plus a comment for a finding that needed no change. Same-repo gating. `Get PR details` now emits an `is_fork` output, and the PR-head checkout and the `Run Claude Code` step are both gated on it. A fork PR is declined outright — no checkout, no agent run, with the reason written to the step summary — rather than falling through to a "metadata-only" review of the base tree, which would trade an untrusted-code problem for a wrong-tree one. A deleted fork (`head.repo` null) counts as a fork. The head checkout also drops its `repository:` input: only a same-repo head reaches that step now, and checkout's default `github.repository` is a value no PR can influence. SHA pins. actions/checkout, actions/github-script, and claude-code-action were all on mutable major tags in a job that holds ANTHROPIC_API_KEY and grants the agent Bash. Each is now pinned to a full commit SHA with the trailing `# vX.Y.Z` comment Dependabot reads, so upgrade automation is unaffected. Pinned to the v7 / v9 SHAs so this does not regress the bumps in the open Dependabot PR #1922. The failure fallback Copilot flagged turns out not to exist: `Checkout repository` carries no status-check function, so GitHub applies an implicit `success()` and skips it after a failed lookup regardless of the `outcome` comparison. Its condition is now the explicit `== 'skipped'` and says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SW1p8E2uiyyLx4RKwrwSrt
There was a problem hiding this comment.
Pull request overview
Hardens the Claude workflow against fork PR execution and mutable action tags.
Changes:
- Declines fork PRs before checkout or Claude execution.
- Pins all actions to verified commit SHAs.
- Clarifies safe checkout behavior for non-PR and failed-lookup paths.
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Copilot review round 1 — no findingsCopilot's review ( Its summary reads the diff correctly on all three axes:
Nothing to respond to and nothing to change, so no follow-up review round is needed. For the record, the two things a reviewer would most reasonably push back on, and why they are deliberate: Declining a fork PR outright rather than degrading to a base-tree review. The metadata-only fallback floated in #1882 would swap an untrusted-code problem for a wrong-tree one — a confident review of a tree that isn't the one under review. Declining visibly, with the reason in the step summary, is the honest failure mode. Dropping |
Closes #1882
Hardens
.github/workflows/claude.ymlin two ways, and documents a third finding that turned out to need no code change.The issue was originally filed as a fork-PR concern raised by Copilot on #1869. The repo runs
pull_request_creation_policy: collaborators_only(verified against the API), which retires most of that threat model — so this is defense in depth, not remediation, and #1882 was re-scored Low to match. It is still worth landing: the mitigation currently lives entirely in a repo setting, flippable from a UI with no review and no diff, rather than in the file.1. Same-repo gating
Get PR detailsnow emits anis_forkoutput, and both the PR-head checkout andRun Claude Codeare gated on it:steps.pr.outcomeis_forkskippedsuccessfalsesuccesstruefailureDeclining a fork PR outright is deliberate. The alternative floated in the issue — fall through to a "metadata-only" review against the base tree — trades an untrusted-code problem for a wrong-tree one, which is the other half of what Copilot flagged. Better to decline visibly than to post a confident review of a tree that isn't the one under review.
A deleted fork counts as a fork:
head.repoisnullin that case, so the comparison treats it as not-ours rather than throwing.The head checkout also drops its
repository:input. Only a same-repo head reaches that step now, and checkout's default isgithub.repository— a value no PR can influence. That is a stronger position than passing a PR-derived repo name that happens to have been checked.2. Actions pinned to commit SHAs
actions/checkout,actions/github-script, andanthropics/claude-code-actionwere all on mutable major tags, in a job that holdsANTHROPIC_API_KEYand grants the agentBash. A force-moved tag on any of the three is an unreviewed code change inside a secret-holding job.Each is now pinned to a full commit SHA with the trailing
# vX.Y.Zcomment Dependabot reads, so pinning costs no upgrade automation:actions/checkout3d3c42e5aac5ba805825da76410c181273ba90b1actions/github-script3a2844b7e9c422d3c10d287c895573f7108da1b3anthropics/claude-code-action5ef2e550a465a721f4f45e4a7d3c340c873e1dccv1)3. The failure fallback — no change needed
Copilot's mirror-image finding was that a failed PR lookup falls back to the base checkout but still runs Claude, reviewing the wrong tree while spending secrets and minutes. On inspection this doesn't happen:
Checkout repositorywas guarded byif: steps.pr.outcome != 'success', which contains no status-check function, so GitHub applies an implicitsuccess()and skips the step after a failed prior step regardless of theoutcomecomparison.The condition is now the explicit
steps.pr.outcome == 'skipped', with a comment saying why the failure case is excluded — so the next reader doesn't have to re-derive it.Out of scope
Copilot's other comment on #1869 — that
issues: read/pull-requests: readprevents@claudefrom posting — is incorrect and is untouched here.claude-code-actionmints its own GitHub App token viaid-token: write; thepermissions:block scopesGITHUB_TOKEN, not that app token.claude[bot]posted on #1825 under exactly these permissions. The read-only grants are correct and must not be widened.Testing
No test surface — this is a workflow file, and workflows only run from the default branch, so the change isn't exercised until
v2/mainreachesmainat the next milestone merge.npm run verify:format-coveragepasses (939 tracked source files gated;.ymlis outside the format globs, so nothing else in the gate applies to this diff).After it reaches
main: verify with a real@claudeinvocation on a same-repo PR (must still check out the PR head and review it), and confirm a fork PR is declined with the summary message rather than silently reviewing the wrong tree.No UI change, so no screenshots.
🤖 Generated with Claude Code
https://claude.ai/code/session_01SW1p8E2uiyyLx4RKwrwSrt