Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,21 @@ on:
types: [submitted]

jobs:
authorize:
runs-on: ubuntu-latest
steps:
- name: Check actor authorization
run: |
ASSOCIATION="${{ github.event.comment.author_association || github.event.review.author_association || github.event.issue.author_association }}"
echo "Author association: $ASSOCIATION"
if [[ "$ASSOCIATION" != "OWNER" && "$ASSOCIATION" != "MEMBER" && "$ASSOCIATION" != "COLLABORATOR" ]]; then
echo "Unauthorized: actor '${{ github.actor }}' has association '$ASSOCIATION'"
exit 1
fi
Comment on lines +18 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 For issues: assigned events, github.event.issue.author_association reflects the original issue creator's association, not the person who triggered the assignment (github.event.sender). If a team member assigns an issue opened by an external user mentioning @claude, the authorize job sees the external user's association (e.g., NONE) and incorrectly rejects the action. Consider using the GitHub API to resolve github.event.sender.login's association for issues events, or handle assigned separately.

Extended reasoning...

What the bug is

The new authorize job determines the actor's association using a fallback chain:

github.event.comment.author_association || github.event.review.author_association || github.event.issue.author_association

For issues: assigned events, there is no comment or review in the payload, so both of those fields evaluate to empty strings. The expression falls through to github.event.issue.author_association, which is the association of the issue creator, not the person who performed the assignment.

Step-by-step proof

  1. An external user (association = NONE) opens an issue with @claude in the body.
  2. A team MEMBER sees the issue and assigns it (triggering the issues: assigned event).
  3. The GitHub webhook payload for this event has:
    • github.event.sender.login = the team member (the assigner)
    • github.event.issue.author_association = NONE (the external user who created the issue)
    • github.event.comment = null (no comment involved)
    • github.event.review = null (no review involved)
  4. The authorize job evaluates: '' || '' || 'NONE' which resolves to ASSOCIATION='NONE'
  5. The if condition checks NONE != OWNER && NONE != MEMBER && NONE != COLLABORATOR - all true, so exit 1
  6. The authorize job fails, blocking the downstream claude job even though the actual actor is a trusted team member.

Why existing code doesn't prevent this

The fallback chain comment.author_association || review.author_association || issue.author_association correctly handles issue_comment, pull_request_review_comment, and pull_request_review events because in those cases author_association on the comment/review reflects the sender. For issues: opened, it also happens to work because the issue author IS the sender. But for issues: assigned, the sender (assigner) is a different person than the issue author, and there is no field in the fallback chain that captures the assigner's association.

Impact

This bug means that the common workflow of an external contributor opening an issue mentioning @claude and then a team member assigning it will be blocked by the authorization check. The team member's legitimate action is rejected because the wrong principal's association is checked.

How to fix

The fix should resolve the sender's association rather than relying on issue.author_association for issues events. Options include:

  • Using the GitHub API (GET /repos/{owner}/{repo}/collaborators/{username}/permission) to check github.event.sender.login's actual association.
  • Adding a conditional branch that handles the assigned event type differently, e.g., checking github.actor against a known collaborator list or API call.
  • Using github.event.sender information which is always the person who triggered the event.

echo "Authorized: actor '${{ github.actor }}'"

claude:
needs: authorize
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')) ||
Expand All @@ -37,6 +51,7 @@ jobs:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

claude-review:
needs: authorize
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@review')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@review'))
Expand Down
Loading