Skip to content

Skip claude-review on fork PRs instead of failing - #2300

Merged
JSv4 merged 1 commit into
mainfrom
fix/claude-review-skip-fork-prs
Sep 6, 2026
Merged

JSv4 merged 1 commit into
mainfrom
fix/claude-review-skip-fork-prs

Conversation

@JSv4

@JSv4 JSv4 commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

claude-review fails on every fork-based PR, and the error message points at the wrong thing.

Observed on #2296 (run 33611167884):

##[error] Could not fetch an OIDC token.
          Did you remember to add `id-token: write` to your workflow permissions?

.github/workflows/claude-code-review.yml does declare id-token: write. GitHub dropped it, because the head repo is a fork. The effective grant on that run:

##[group]GITHUB_TOKEN Permissions
Contents: read   Issues: read   Metadata: read   PullRequests: read
Secret source: None

No IdToken, everything read-only, no secrets. Anyone debugging from the error text alone will go edit a file that is already correct.

Three independent blockers, any one fatal

# Blocker Evidence in run log
1 id-token: write downgraded → no OIDC ACTIONS_ID_TOKEN_REQUEST_URL missing; 3 retries, all fail
2 Secret store withheld Secret source: None, "claude_code_oauth_token": ""
3 GITHUB_TOKEN read-only the prompt's gh pr comment could not post regardless

Two things people assume will fix it, which don't:

  • "Approve and run." That run is run_attempt: 2, triggering_actor: JSv4 — already maintainer-approved — and still shows Secret source: None. Approval lets an untrusted workflow start; it does not hand it credentials.
  • A stale secret. The dependabot run one day earlier (33788653880) shows the happy path end to end: OIDC token successfully obtained → Exchanging OIDC token for app token... → App token successfully obtained → Using GITHUB_TOKEN from OIDC. The secret is fine.

Across the last 40 runs of this workflow, every non-fork branch succeeds and the only two non-successes are the only two fork PRs (#2296 failure, and fix/local-session-restoration still sitting at action_required).

Change

One line on the job, plus a comment recording why:

if: github.event.pull_request.head.repo.full_name == github.repository

The job now reports skipped on fork PRs — which is accurate, it genuinely cannot run — instead of a red X the contributor has no way to clear and no fault in. Dependabot is unaffected: its branches live in this repository, so the guard passes and the OIDC exchange works exactly as before.

The commented-out "Optional: Filter by PR author" scaffolding it replaces is removed rather than left alongside.

Why not pull_request_target

That's the obvious fix and it's a trap, so the comment in the workflow says so explicitly. pull_request_target runs in base-repo context with full secrets, OIDC and a write-capable token — and reviewing a PR means checking out the untrusted head. On a frontend/ PR a single yarn install is postinstall RCE against the org's CLAUDE_CODE_OAUTH_TOKEN.

Even without code execution, the diff is attacker-controlled input the model reads. The action's own docs say it (allowed_non_write_users): "Processing untrusted content exposes the workflow to prompt injection... best-effort scrub... reduces but does not eliminate." The narrow claude_args allowlist (gh subcommands only) is what holds the line today; pull_request_target would pair injected text with a write-capable token.

To review a fork PR on demand, comment @claude on it — .github/workflows/claude.yml fires on issue_comment, which runs in base context with secrets intact, and the action gates on the commenter's write permission, so maintainers can invoke it and contributors can't.

Scope

Only claude-code-review.yml. CODECOV_TOKEN is the other non-default secret in .github/workflows/, used by backend.yml, frontend.yml, codecov-notify.yml and the three frontend-e2e-*.yml files; those are untouched here and worth a separate look if fork PRs turn out to trip them too.

Verification

  • pre-commit run --files .github/workflows/claude-code-review.yml changelog.d/claude-review-fork-skip.fixed.md → all hooks pass (check yaml, validate changelog fragments).
  • yaml.safe_load on the workflow parses; the guard lands on jobs.claude-review.if and permissions is unchanged.
  • The guard's positive case is exercised by this PR itself: claude-review ran (34009576994) with Secret source: Actions, OIDC token successfully obtained, Exchanging OIDC token for app token — i.e. the if: evaluated true and the job reached the authenticated path. The negative case can only be exercised by an actual fork PR; fix(frontend): stabilize label-set workflows #2296 will exercise it on its next sync after this merges.

One thing this run surfaced that is not fixed here

That job reports pass without having reviewed anything:

##[warning] Skipping action due to workflow validation: Workflow validation failed.
            The workflow file must exist and have identical content to the version
            on the repository...
Action skipped due to workflow validation error.
Exiting due to workflow validation skip
outcome=success; conclusion=success

claude-code-action refuses to run when the PR modifies the workflow that invokes it — sensible, since a PR could otherwise rewrite the review prompt — but it exits success, not skipped. So every PR touching .github/workflows/ gets a green claude-review with no review behind it, and nothing in the check name says so. Pre-existing upstream behavior, orthogonal to this change, and worth its own issue rather than scope creep here.

GitHub clamps pull_request runs whose head is a fork: the id-token: write
this workflow declares is silently dropped and the secret store is
withheld (Secret source: None, claude_code_oauth_token: ""). The action
then dies on "Could not fetch an OIDC token. Did you remember to add
`id-token: write` to your workflow permissions?" — an error that accuses
the workflow file for a permission it already declares, sending anyone
debugging from the message alone to edit correct code. Approving the run
does not restore secrets, and GITHUB_TOKEN is read-only on fork PRs, so
the `gh pr comment` the job is asked to make could not post either.

Guard the job on head.repo.full_name == github.repository so it reports
skipped — which is what actually happens — rather than a red X no
contributor can clear. Dependabot branches live in this repository, so
they pass the guard and the OIDC exchange works unchanged.

Observed on run 33611167884 (PR #2296); dependabot control is run
33788653880.
@JSv4
JSv4 merged commit cbbd9df into main Sep 6, 2026
12 checks passed
@JSv4
JSv4 deleted the fix/claude-review-skip-fork-prs branch September 6, 2026 03:45
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 6, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant