Skip to content

ci: harden claude.yml fork-PR checkout and same-repo gating #1882

Description

@cliffhall

Raised by GitHub Copilot during review of #1869 (the byte-for-byte restore of .github/workflows/claude.yml). Deliberately not fixed there — that PR's contract is a faithful restore of a known-working file, so a behavior change belongs in its own PR.

The observation

.github/workflows/claude.yml checks out the PR head repo:

- name: Checkout PR branch
  if: steps.pr.outcome == 'success'
  uses: actions/checkout@v6
  with:
    ref: ${{ steps.pr.outputs.sha }}
    repository: ${{ steps.pr.outputs.repo }}
    fetch-depth: 0

steps.pr.outputs.repo is pr.data.head.repo.full_name, which for a fork PR is the fork. The Run Claude Code step then runs in that checkout with ANTHROPIC_API_KEY in the environment and --allowedTools "Bash,mcp__mcp-docs". So a fork's package.json scripts, tool configs, or repo-local agent instructions are in reach of a tool-enabled agent run holding a secret.

Copilot also flagged the mirror-image case: when the PR lookup step is skipped or fails, the workflow falls back to checking out the base repo but still runs Claude — so it can review the wrong tree while still spending secrets and minutes.

Why it is not urgent

Two structural mitigations are already in place:

  1. Author-association gate. Every trigger arm requires the commenting/opening user to be OWNER / MEMBER / COLLABORATOR. An outside contributor cannot self-trigger the workflow on their own PR.
  2. pull_request_creation_policy: collaborators_only. The repo does not accept PRs from non-collaborators at all (consistent with the issues-only policy in CONTRIBUTORS.md, go-live 6/9: restrict PRs to collaborators; CONTRIBUTORS.md + SECURITY.md #1820), so untrusted fork PRs largely do not exist here.

The residual path is a maintainer deliberately running @claude against a collaborator's fork branch — collaborators already hold repo access, so the marginal exposure is small.

Possible hardening

Worth evaluating, none obviously free:

  • Gate the PR-head checkout on head.repo.full_name == github.repository, letting fork PRs fall through to a metadata-only review (no code checkout).
  • Alternatively gate the Run Claude Code step itself on same-repo, so the fallback branch cannot review the wrong tree.
  • Consider pinning actions/checkout, actions/github-script, and anthropics/claude-code-action to commit SHAs rather than mutable major tags.

Any change here should be verified against a real @claude invocation afterward, since the workflow is only exercised from the default branch.

Out of scope

Copilot's other comment on #1869 — that issues: read / pull-requests: read prevents @claude from posting — is factually incorrect and needs no action. claude-code-action mints its own GitHub App token via id-token: write; the permissions: block scopes GITHUB_TOKEN, not that app token. claude[bot] posted on #1825 on 2026-07-27 under exactly these permissions, ten days after ac3c1a12 (2026-07-17) last touched the file. The read-only grants are correct and must not be widened.

Metadata

Metadata

Assignees

Labels

v2Issues and PRs for v2

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions