name: Claude Code Review # Uses pull_request_target so the workflow runs as defined on the default branch, # which allows access to secrets for posting review comments on fork PRs. # SECURITY: The checkout pins the fork's HEAD SHA (not the branch name) to # prevent TOCTOU races (force-push between trigger and checkout). The # claude-code-action sandboxes execution — it does NOT run arbitrary code # from the checked-out source. on: # Trigger only when explicitly requested: # - Add the "claude-review" label to a PR, OR # - Comment "@claude" or "/review" on a PR pull_request_target: types: [labeled] issue_comment: types: [created] # Serialize per-PR to avoid racing review comments. concurrency: group: claude-review-${{ github.event.issue.number || github.event.pull_request.number }} cancel-in-progress: false jobs: claude-review: # Run only when: # 1. The "claude-review" label is added to a non-draft PR by a trusted contributor, OR # 2. A trusted contributor comments "@claude" or "/review" on a PR if: | ( github.event_name == 'pull_request_target' && github.event.label.name == 'claude-review' && github.event.pull_request.draft == false && (github.event.pull_request.author_association == 'OWNER' || github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR') ) || ( github.event_name == 'issue_comment' && github.event.issue.pull_request && (contains(github.event.comment.body, '@claude') || contains(github.event.comment.body, '/review')) && (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR') ) runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: read pull-requests: write issues: read id-token: write steps: # For issue_comment triggers, resolve the PR number, head SHA, and fork repo - name: Resolve PR context id: pr uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 with: script: | let pr; if (context.eventName === 'issue_comment') { const resp = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: context.payload.issue.number, }); pr = resp.data; } else { pr = context.payload.pull_request; } core.setOutput('number', pr.number); core.setOutput('sha', pr.head.sha); core.setOutput('repo', pr.head.repo.full_name); core.setOutput('branch', pr.head.ref); - name: Checkout PR head uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: repository: ${{ steps.pr.outputs.repo }} ref: ${{ steps.pr.outputs.sha }} fetch-depth: 1 - name: Run Claude Code Review id: claude-review uses: anthropics/claude-code-action@9469d113c6afd29550c402740f22d1a97dd1209b # v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }} allowed_non_write_users: '*' show_full_output: true plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' plugins: 'code-review@claude-code-plugins' prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ steps.pr.outputs.number }}'