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

choreMaintenance: deps, build tooling, CI, cleanup — no user-facing behavior changev2Issues and PRs for v2

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions