-
Notifications
You must be signed in to change notification settings - Fork 372
Harden Claude workflow permission gate #776
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,13 +11,104 @@ on: | |
| types: [submitted] | ||
|
|
||
| jobs: | ||
| claude: | ||
| authorize_claude_actor: | ||
| 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_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || | ||
| (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| outputs: | ||
| authorized: ${{ steps.permission-gate.outputs.authorized }} | ||
| steps: | ||
| - name: Verify actor can run Claude | ||
| id: permission-gate | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 | ||
| with: | ||
| script: | | ||
| const owner = context.repo.owner; | ||
| const repo = context.repo.repo; | ||
| const permissionByLogin = new Map(); | ||
|
|
||
| function claudeRequester() { | ||
| if ( | ||
| context.eventName === 'issue_comment' || | ||
| context.eventName === 'pull_request_review_comment' | ||
| ) { | ||
| return context.payload.comment?.user?.login; | ||
| } | ||
|
|
||
| if (context.eventName === 'pull_request_review') { | ||
| return context.payload.review?.user?.login; | ||
| } | ||
|
|
||
| if (context.eventName === 'issues') { | ||
| return context.payload.issue?.user?.login; | ||
| } | ||
|
Comment on lines
+47
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an |
||
|
|
||
| return null; | ||
| } | ||
|
|
||
| async function hasWriteAccessFor(username) { | ||
| if (!username) { | ||
| return false; | ||
| } | ||
|
|
||
| if (permissionByLogin.has(username)) { | ||
| return permissionByLogin.get(username); | ||
| } | ||
|
|
||
| let hasWriteAccess = false; | ||
| try { | ||
| const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ | ||
| owner, | ||
| repo, | ||
| username | ||
| }); | ||
| hasWriteAccess = ['admin', 'write'].includes(permission.permission); | ||
| } catch (error) { | ||
| core.warning( | ||
| `Unable to verify ${username} write permission for Claude; ` + | ||
| `treating the Claude request as untrusted: ${error.message || error}` | ||
| ); | ||
| } | ||
|
|
||
| permissionByLogin.set(username, hasWriteAccess); | ||
| return hasWriteAccess; | ||
| } | ||
|
|
||
| const actor = context.actor; | ||
| const requester = claudeRequester(); | ||
|
|
||
| if (!requester) { | ||
| core.setOutput('authorized', 'false'); | ||
| core.setFailed('Unable to run Claude because the requester could not be determined.'); | ||
| return; | ||
| } | ||
|
|
||
| if (!(await hasWriteAccessFor(actor))) { | ||
| core.setOutput('authorized', 'false'); | ||
| core.setFailed(`Unable to run Claude because ${actor} does not have write/admin repository permission.`); | ||
| return; | ||
| } | ||
|
|
||
| if (!(await hasWriteAccessFor(requester))) { | ||
| core.setOutput('authorized', 'false'); | ||
| core.setFailed( | ||
| `Unable to run Claude because requester ${requester} does not have write/admin repository permission.` | ||
| ); | ||
| return; | ||
| } | ||
|
Comment on lines
+91
to
+103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| core.setOutput('authorized', 'true'); | ||
| core.info(`Authorized ${actor} and Claude requester ${requester} to run Claude.`); | ||
|
|
||
| claude: | ||
| needs: authorize_claude_actor | ||
| if: needs.authorize_claude_actor.outputs.authorized == 'true' | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice to pin
actions/github-scriptto a full commit SHA rather than a mutable tag — good supply-chain hygiene. Worth double-checking (outside this sandbox, which blocked outboundgh apicalls to other repos) that3a2844b7e9c422d3c10d287c895573f7108da1b3is actually the commit taggedv9upstream, since a stale/incorrect comment here would be misleading if this pin is ever rotated by hand later.