From 9017972263dd283a1a87b5a821232296989cb3de Mon Sep 17 00:00:00 2001 From: Keith Kraus Date: Wed, 6 May 2026 11:07:35 -0400 Subject: [PATCH 1/3] Add Claude Code GitHub Action workflow Adds the standard `anthropics/claude-code-action@v1` workflow so maintainers can invoke Claude on issues and PRs via `@claude`. The job's `if:` condition restricts triggering to actors whose `author_association` on the comment/review/issue is `OWNER`, `MEMBER`, or `COLLABORATOR` so that only repository maintainers can grant Claude the read/write access this workflow exposes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/claude.yml | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000000..38ec447f54 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + pull_request_review: + types: [submitted] + issues: + types: [opened, assigned] + +jobs: + claude: + # Only trigger when "@claude" is mentioned AND the actor has write access + # to the repository (OWNER, MEMBER, or COLLABORATOR). This restricts the + # ability to invoke Claude — which can read and modify repository contents + # — to repository maintainers only. + if: | + (github.event_name == 'issue_comment' && + contains(github.event.comment.body, '@claude') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review_comment' && + contains(github.event.comment.body, '@claude') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review' && + contains(github.event.review.body, '@claude') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) || + (github.event_name == 'issues' && + (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association)) + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@62238ddb33772a079b0a6d8665a1ff3043583067 # v1.0.114 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} From a86790de1bfc4459eb3472691a7b72fc631416aa Mon Sep 17 00:00:00 2001 From: Keith Kraus Date: Wed, 6 May 2026 11:42:26 -0400 Subject: [PATCH 2/3] Tighten Claude workflow access gate --- .github/workflows/claude.yml | 53 ++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 38ec447f54..c79acf5b9a 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -15,24 +15,49 @@ on: types: [opened, assigned] jobs: - claude: - # Only trigger when "@claude" is mentioned AND the actor has write access - # to the repository (OWNER, MEMBER, or COLLABORATOR). This restricts the - # ability to invoke Claude — which can read and modify repository contents - # — to repository maintainers only. + authorize: + # Only start this job for "@claude" mentions. The first step below checks + # the triggering actor's actual repository permission before the workflow + # grants write permissions or exposes the Anthropic API key. if: | (github.event_name == 'issue_comment' && - contains(github.event.comment.body, '@claude') && - contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review_comment' && - contains(github.event.comment.body, '@claude') && - contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review' && - contains(github.event.review.body, '@claude') && - contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) || + contains(github.event.review.body, '@claude')) || (github.event_name == 'issues' && - (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) && - contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association)) + (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + has_write_access: ${{ steps.actor-permission.outputs.has_write_access }} + steps: + - name: Check actor repository permission + id: actor-permission + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + TRIGGERING_ACTOR: ${{ github.actor }} + run: | + set -euo pipefail + + permission="$(gh api "repos/${GH_REPO}/collaborators/${TRIGGERING_ACTOR}/permission" --jq '.permission' 2>/dev/null || true)" + + case "${permission}" in + admin|maintain|write) + echo "has_write_access=true" >> "$GITHUB_OUTPUT" + ;; + *) + echo "has_write_access=false" >> "$GITHUB_OUTPUT" + echo "Skipping Claude because ${TRIGGERING_ACTOR} does not have write access to ${GH_REPO}." + ;; + esac + + claude: + needs: authorize + if: needs.authorize.outputs.has_write_access == 'true' runs-on: ubuntu-latest permissions: contents: write @@ -42,7 +67,7 @@ jobs: actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 From f6ec70f007227e922d27c0d8986fd34156ad6670 Mon Sep 17 00:00:00 2001 From: Keith Kraus Date: Wed, 6 May 2026 11:52:13 -0400 Subject: [PATCH 3/3] Pre-filter Claude workflow triggers --- .github/workflows/claude.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index c79acf5b9a..f092ed3bd6 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -12,22 +12,27 @@ on: pull_request_review: types: [submitted] issues: - types: [opened, assigned] + types: [opened] jobs: authorize: - # Only start this job for "@claude" mentions. The first step below checks + # Use author_association as a cheap pre-filter so most unauthorized + # "@claude" mentions do not start a runner. The step below still checks # the triggering actor's actual repository permission before the workflow # grants write permissions or exposes the Anthropic API key. if: | (github.event_name == 'issue_comment' && - contains(github.event.comment.body, '@claude')) || + contains(github.event.comment.body, '@claude') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || (github.event_name == 'pull_request_review_comment' && - contains(github.event.comment.body, '@claude')) || + contains(github.event.comment.body, '@claude') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || (github.event_name == 'pull_request_review' && - contains(github.event.review.body, '@claude')) || + contains(github.event.review.body, '@claude') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) || (github.event_name == 'issues' && - (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association)) runs-on: ubuntu-latest permissions: contents: read