diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 449f0b8..b928025 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -1,5 +1,10 @@ name: Claude Code Review +# The reviewer only needs to read the diff and post comments — contents stays +# read-only, which is the fork-PR hardening that matters (fork PRs never +# receive the OAuth token secret either). id-token: write is required: +# claude-code-action fetches an OIDC token at startup even with OAuth-token +# auth, and without the grant the action dies before the review starts. permissions: contents: read pull-requests: write diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 8577de4..17df33b 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -4,6 +4,8 @@ permissions: contents: write pull-requests: write issues: write + # claude-code-action fetches an OIDC token at startup even with OAuth-token + # auth — without this grant the action dies before doing anything. id-token: write on: @@ -14,9 +16,21 @@ on: jobs: claude: + # Any GitHub user can comment on a public repo, so the @claude trigger must + # be gated to people we trust with the workflow's write permissions. + # OWNER only fires on user-owned repos — this is an org repo, where org + # members report MEMBER — so MEMBER is the arm that actually admits the + # maintainers. It covers ANY org member (including read-only ones); fine + # for a small org, but swap in a `gh api .../permission` gate step if the + # org ever outgrows the set of people who should hold contents: write. + # The gate trusts the COMMENTER, not the content — a trusted commenter can + # still point @claude at untrusted PR text; that residual is inherent. if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) + (github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR') && + ((github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude'))) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4