From dcaf313d9c3d57b2d82603cbc36d53baad9d3a39 Mon Sep 17 00:00:00 2001 From: Simon Heimlicher Date: Thu, 8 Jan 2026 15:26:13 +0100 Subject: [PATCH 1/3] fix(workflows): add authorization checks to prevent token abuse SECURITY FIX: Addresses critical vulnerability where any external user could trigger Claude workflows using CLAUDE_CODE_OAUTH_TOKEN. Changes: - Add author_association checks to claude.yml (OWNER, MEMBER, COLLABORATOR only) - Add author_association checks to claude-code-review.yml - Add concurrency controls to prevent workflow spam Resolves: https://github.com/simonheimlicher/spx-cli/pull/1#issuecomment-3724060006 --- .github/workflows/claude-code-review.yml | 12 ++++++------ .github/workflows/claude.yml | 18 +++++++++++++----- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 8452b0f..f1beb8f 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -12,11 +12,12 @@ on: jobs: claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + concurrency: + group: claude-review-${{ github.event.pull_request.number }} + cancel-in-progress: false + # Only run for trusted contributors (OWNER, MEMBER, COLLABORATOR) + if: | + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) runs-on: ubuntu-latest permissions: @@ -54,4 +55,3 @@ jobs: # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://code.claude.com/docs/en/cli-reference for available options claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' - diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index d300267..e56a5e8 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -12,11 +12,20 @@ on: jobs: claude: + concurrency: + group: claude-${{ github.event.issue.number || github.event.pull_request.number }} + cancel-in-progress: false 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'))) + ( + (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.sender.author_association)) + ) runs-on: ubuntu-latest permissions: contents: read @@ -47,4 +56,3 @@ jobs: # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://code.claude.com/docs/en/cli-reference for available options # claude_args: '--allowed-tools Bash(gh pr:*)' - From f2d3f3ca55e2d8d9124199b076384aeae481781c Mon Sep 17 00:00:00 2001 From: Simon Heimlicher Date: Thu, 8 Jan 2026 15:50:51 +0100 Subject: [PATCH 2/3] refactor(workflows): parameterize configuration with GitHub Actions variables Extract repeated configuration values into GitHub Actions variables with sensible defaults, enabling runtime configuration without code changes. Configuration variables: - CLAUDE_AUTHORIZED_ROLES: Control who can trigger workflows - CLAUDE_MENTION_TRIGGER: Customize trigger word (default: @claude) - CLAUDE_CONCURRENCY_CANCEL: Enable canceling in-progress runs - CLAUDE_ALLOWED_TOOLS: Restrict tool access - CLAUDE_CUSTOM_PROMPT: Override default prompts - CLAUDE_REVIEW_*: Separate configuration for code review workflow Benefits: - DRY: Single source of truth for authorization roles (was repeated 5x) - Flexibility: Change behavior via repo settings without editing workflows - Security: Easily adjust authorization without touching code - Documentation: Comprehensive guide in .github/CLAUDE_WORKFLOWS.md Related: https://github.com/simonheimlicher/spx-cli/pull/1#discussion_r1 --- .github/CLAUDE_WORKFLOWS.md | 201 +++++++++++++++++++++++ .github/workflows/claude-code-review.yml | 60 ++++--- .github/workflows/claude.yml | 37 +++-- 3 files changed, 265 insertions(+), 33 deletions(-) create mode 100644 .github/CLAUDE_WORKFLOWS.md diff --git a/.github/CLAUDE_WORKFLOWS.md b/.github/CLAUDE_WORKFLOWS.md new file mode 100644 index 0000000..5c1922a --- /dev/null +++ b/.github/CLAUDE_WORKFLOWS.md @@ -0,0 +1,201 @@ +# Claude Code GitHub Workflows Configuration + +This repository uses two GitHub Actions workflows to integrate Claude Code: + +1. **`claude.yml`** - Interactive Claude assistant triggered by `@claude` mentions +2. **`claude-code-review.yml`** - Automatic code review on pull requests + +## Security + +Both workflows include authorization checks to prevent unauthorized access to the `CLAUDE_CODE_OAUTH_TOKEN`. Only trusted contributors can trigger Claude workflows. + +## Configuration Variables + +Configure these workflows via **Settings → Secrets and variables → Actions → Variables** in your repository. + +### Shared Security Settings + +#### `CLAUDE_AUTHORIZED_ROLES` (claude.yml) + +- **Type:** JSON array +- **Default:** `["OWNER", "MEMBER", "COLLABORATOR"]` +- **Description:** GitHub author associations allowed to trigger `@claude` mentions +- **Example:** `["OWNER", "MEMBER"]` (restrict to owners and members only) +- **Security:** This is the primary security control. Only change if you understand the implications. + +#### `CLAUDE_REVIEW_AUTHORIZED_ROLES` (claude-code-review.yml) + +- **Type:** JSON array +- **Default:** `["OWNER", "MEMBER", "COLLABORATOR"]` +- **Description:** GitHub author associations allowed to trigger auto-reviews +- **Example:** `["OWNER"]` (only repository owners) + +### Claude Assistant Settings (claude.yml) + +#### `CLAUDE_MENTION_TRIGGER` + +- **Type:** String +- **Default:** `@claude` +- **Description:** Text that triggers the Claude workflow +- **Example:** `@bot`, `@ai`, or any custom trigger word + +#### `CLAUDE_CONCURRENCY_CANCEL` + +- **Type:** String (boolean) +- **Default:** `false` +- **Description:** Whether to cancel in-progress Claude runs when new mention arrives +- **Values:** `true` or `false` +- **Use case:** Set to `true` if you want latest request to cancel previous ones + +#### `CLAUDE_CUSTOM_PROMPT` + +- **Type:** String (multiline supported) +- **Default:** Empty (Claude follows instructions from the comment) +- **Description:** Override default behavior with a custom prompt +- **Example:** + ``` + You are a helpful code assistant. Always: + - Reference CLAUDE.md for project standards + - Run validation before committing + - Be concise in responses + ``` + +#### `CLAUDE_ALLOWED_TOOLS` + +- **Type:** String +- **Default:** Empty (unrestricted access) +- **Description:** Restrict which tools Claude can use +- **Example:** `--allowed-tools "Bash(gh pr:*),Read,Edit"` +- **Security:** Restrict to specific commands to limit what Claude can do +- **See:** [Claude Code tool documentation](https://code.claude.com/docs/en/cli-reference) + +### Code Review Settings (claude-code-review.yml) + +#### `CLAUDE_REVIEW_CONCURRENCY_CANCEL` + +- **Type:** String (boolean) +- **Default:** `false` +- **Description:** Whether to cancel in-progress reviews on new PR updates +- **Values:** `true` or `false` +- **Use case:** Set to `true` to cancel old review when PR is updated + +#### `CLAUDE_REVIEW_CUSTOM_PROMPT` + +- **Type:** String (multiline supported) +- **Default:** Pre-configured review prompt (see workflow file) +- **Description:** Custom review instructions for Claude +- **Example:** + ``` + Review this PR focusing on: + - TypeScript type safety + - Test coverage for new features + - Security vulnerabilities + + Reference CLAUDE.md for coding standards. + Use gh pr comment to post your review. + ``` + +#### `CLAUDE_REVIEW_ALLOWED_TOOLS` + +- **Type:** String +- **Default:** `--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"` +- **Description:** Restrict which tools Claude can use during reviews +- **Security:** Default restricts to read-only gh commands + commenting +- **Example:** To make read-only: Remove `Bash(gh pr comment:*)` from the list + +## Configuration Examples + +### Example 1: Restrict to Repository Owners Only + +``` +CLAUDE_AUTHORIZED_ROLES = ["OWNER"] +CLAUDE_REVIEW_AUTHORIZED_ROLES = ["OWNER"] +``` + +### Example 2: Use Custom Trigger Word + +``` +CLAUDE_MENTION_TRIGGER = @bot +``` + +### Example 3: Cancel In-Progress Runs + +``` +CLAUDE_CONCURRENCY_CANCEL = true +CLAUDE_REVIEW_CONCURRENCY_CANCEL = true +``` + +### Example 4: Restrict Claude Tools (High Security) + +``` +CLAUDE_ALLOWED_TOOLS = --allowed-tools "Read,Grep,Glob,Bash(gh pr:*)" +CLAUDE_REVIEW_ALLOWED_TOOLS = --allowed-tools "Read,Grep,Glob,Bash(gh pr view:*),Bash(gh pr diff:*)" +``` + +### Example 5: Custom Review Prompt + +``` +CLAUDE_REVIEW_CUSTOM_PROMPT = +REPO: ${{ github.repository }} +PR NUMBER: ${{ github.event.pull_request.number }} + +Focus your review on: +1. Code matches docs/code/typescript.md standards +2. All changes have test coverage +3. No security vulnerabilities (SQL injection, XSS, etc.) +4. Performance implications + +Reference CLAUDE.md for full project context. +Post review using: gh pr comment $PR_NUMBER --body "..." +``` + +## How to Set Variables + +1. Go to your repository on GitHub +2. Click **Settings** → **Secrets and variables** → **Actions** +3. Click the **Variables** tab +4. Click **New repository variable** +5. Enter the variable name (e.g., `CLAUDE_AUTHORIZED_ROLES`) +6. Enter the value +7. Click **Add variable** + +## Testing Configuration Changes + +After changing variables, test by: + +1. **For `claude.yml`:** Create a test issue or PR and mention your trigger word (e.g., `@claude`) +2. **For `claude-code-review.yml`:** Open a new PR or push to an existing PR + +Check the **Actions** tab to see if workflows triggered correctly. + +## Security Best Practices + +1. **Always configure `CLAUDE_AUTHORIZED_ROLES`** - Never allow `CONTRIBUTOR` or `FIRST_TIME_CONTRIBUTOR` +2. **Restrict `CLAUDE_ALLOWED_TOOLS`** - Only give Claude the tools it needs +3. **Review workflow logs** - Periodically check Actions logs for suspicious activity +4. **Rotate tokens** - If you suspect token compromise, regenerate `CLAUDE_CODE_OAUTH_TOKEN` +5. **Use concurrency controls** - Prevents workflow spam attacks + +## Troubleshooting + +### Claude isn't responding to mentions + +- Check that your GitHub author association matches `CLAUDE_AUTHORIZED_ROLES` +- Verify the trigger word matches `CLAUDE_MENTION_TRIGGER` +- Check workflow logs in the Actions tab for errors + +### Reviews aren't running automatically + +- Confirm PR author's association matches `CLAUDE_REVIEW_AUTHORIZED_ROLES` +- Check if workflow is enabled in **Settings → Actions** +- Look for workflow runs in the Actions tab + +### "Unrecognized named-value" warnings in IDE + +These are expected linter warnings. Variables are resolved at runtime by GitHub Actions. + +## Additional Resources + +- [Claude Code Documentation](https://code.claude.com/docs) +- [GitHub Actions Variables](https://docs.github.com/en/actions/learn-github-actions/variables) +- [GitHub author_association values](https://docs.github.com/en/graphql/reference/enums#commentauthorassociation) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index f1beb8f..e986b4d 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -1,5 +1,11 @@ name: Claude Code Review +# Configuration via GitHub Actions Variables (Settings > Secrets and variables > Actions > Variables): +# - CLAUDE_REVIEW_AUTHORIZED_ROLES: JSON array of roles allowed (default: ["OWNER", "MEMBER", "COLLABORATOR"]) +# - CLAUDE_REVIEW_CONCURRENCY_CANCEL: Cancel in-progress reviews (default: false) +# - CLAUDE_REVIEW_ALLOWED_TOOLS: Restrict tool access (default: gh commands only) +# - CLAUDE_REVIEW_CUSTOM_PROMPT: Optional custom review prompt + on: pull_request: types: [opened, synchronize] @@ -14,10 +20,10 @@ jobs: claude-review: concurrency: group: claude-review-${{ github.event.pull_request.number }} - cancel-in-progress: false - # Only run for trusted contributors (OWNER, MEMBER, COLLABORATOR) + cancel-in-progress: ${{ vars.CLAUDE_REVIEW_CONCURRENCY_CANCEL == 'true' }} + # Only run for trusted contributors (configurable via CLAUDE_REVIEW_AUTHORIZED_ROLES) if: | - contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) + contains(fromJSON(vars.CLAUDE_REVIEW_AUTHORIZED_ROLES || '["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) runs-on: ubuntu-latest permissions: @@ -32,26 +38,42 @@ jobs: with: fetch-depth: 1 + # Set default prompt if custom one not provided + - name: Set review prompt + id: set-prompt + run: | + if [ -n "${{ vars.CLAUDE_REVIEW_CUSTOM_PROMPT }}" ]; then + echo "prompt<> $GITHUB_OUTPUT + echo "${{ vars.CLAUDE_REVIEW_CUSTOM_PROMPT }}" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + else + echo "prompt<> $GITHUB_OUTPUT + cat <<'PROMPT_EOF' >> $GITHUB_OUTPUT + REPO: ${{ github.repository }} + PR NUMBER: ${{ github.event.pull_request.number }} + + Please review this pull request and provide feedback on: + - Code quality and best practices + - Potential bugs or issues + - Performance considerations + - Security concerns + - Test coverage + + Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. + + Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. + PROMPT_EOF + echo "EOF" >> $GITHUB_OUTPUT + fi + - name: Run Claude Code Review id: claude-review uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - prompt: | - REPO: ${{ github.repository }} - PR NUMBER: ${{ github.event.pull_request.number }} - - Please review this pull request and provide feedback on: - - Code quality and best practices - - Potential bugs or issues - - Performance considerations - - Security concerns - - Test coverage - - Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. - - Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. + prompt: ${{ steps.set-prompt.outputs.prompt }} + # Tool restrictions (configurable via CLAUDE_REVIEW_ALLOWED_TOOLS variable) + # Default: Only gh commands for reading and commenting # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options - claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' + claude_args: ${{ vars.CLAUDE_REVIEW_ALLOWED_TOOLS || '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' }} diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index e56a5e8..7e3c8ae 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -1,5 +1,12 @@ name: Claude Code +# Configuration via GitHub Actions Variables (Settings > Secrets and variables > Actions > Variables): +# - CLAUDE_AUTHORIZED_ROLES: JSON array of roles allowed to trigger (default: ["OWNER", "MEMBER", "COLLABORATOR"]) +# - CLAUDE_MENTION_TRIGGER: Text to trigger workflow (default: @claude) +# - CLAUDE_CONCURRENCY_CANCEL: Cancel in-progress runs (default: false) +# - CLAUDE_ALLOWED_TOOLS: Restrict tool access (default: unrestricted) +# - CLAUDE_CUSTOM_PROMPT: Optional custom prompt to override default behavior + on: issue_comment: types: [created] @@ -14,17 +21,17 @@ jobs: claude: concurrency: group: claude-${{ github.event.issue.number || github.event.pull_request.number }} - cancel-in-progress: false + cancel-in-progress: ${{ vars.CLAUDE_CONCURRENCY_CANCEL == 'true' }} 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.sender.author_association)) + (github.event_name == 'issue_comment' && contains(github.event.comment.body, vars.CLAUDE_MENTION_TRIGGER || '@claude') && + contains(fromJSON(vars.CLAUDE_AUTHORIZED_ROLES || '["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, vars.CLAUDE_MENTION_TRIGGER || '@claude') && + contains(fromJSON(vars.CLAUDE_AUTHORIZED_ROLES || '["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, vars.CLAUDE_MENTION_TRIGGER || '@claude') && + contains(fromJSON(vars.CLAUDE_AUTHORIZED_ROLES || '["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) || + (github.event_name == 'issues' && (contains(github.event.issue.body, vars.CLAUDE_MENTION_TRIGGER || '@claude') || contains(github.event.issue.title, vars.CLAUDE_MENTION_TRIGGER || '@claude')) && + contains(fromJSON(vars.CLAUDE_AUTHORIZED_ROLES || '["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.sender.author_association)) ) runs-on: ubuntu-latest permissions: @@ -45,14 +52,16 @@ jobs: with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - # This is an optional setting that allows Claude to read CI results on PRs + # Allow Claude to read CI results on PRs additional_permissions: | actions: read - # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. - # prompt: 'Update the pull request description to include a summary of changes.' + # Custom prompt (configurable via CLAUDE_CUSTOM_PROMPT variable) + # If not set, Claude will perform instructions from the comment that tagged it + prompt: ${{ vars.CLAUDE_CUSTOM_PROMPT || '' }} - # Optional: Add claude_args to customize behavior and configuration + # Tool restrictions (configurable via CLAUDE_ALLOWED_TOOLS variable) # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://code.claude.com/docs/en/cli-reference for available options - # claude_args: '--allowed-tools Bash(gh pr:*)' + # Example: '--allowed-tools "Bash(gh pr:*)"' + claude_args: ${{ vars.CLAUDE_ALLOWED_TOOLS || '' }} From c2727c93102956d96fcc65475385c0ff3c1dddb9 Mon Sep 17 00:00:00 2001 From: Simon Heimlicher Date: Thu, 8 Jan 2026 16:31:43 +0100 Subject: [PATCH 3/3] fix(workflows): use correct author_association field for issues event Fix critical bug where issues event used github.event.sender.author_association instead of github.event.issue.author_association. The sender object does not have author_association for issues events, causing the check to always fail. This completely broke @claude mentions in issues (opened, assigned events). Fixes: https://github.com/simonheimlicher/spx-cli/pull/2#issuecomment-3724304944 --- .github/workflows/claude.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 7e3c8ae..3b7d689 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -31,7 +31,7 @@ jobs: (github.event_name == 'pull_request_review' && contains(github.event.review.body, vars.CLAUDE_MENTION_TRIGGER || '@claude') && contains(fromJSON(vars.CLAUDE_AUTHORIZED_ROLES || '["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) || (github.event_name == 'issues' && (contains(github.event.issue.body, vars.CLAUDE_MENTION_TRIGGER || '@claude') || contains(github.event.issue.title, vars.CLAUDE_MENTION_TRIGGER || '@claude')) && - contains(fromJSON(vars.CLAUDE_AUTHORIZED_ROLES || '["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.sender.author_association)) + contains(fromJSON(vars.CLAUDE_AUTHORIZED_ROLES || '["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association)) ) runs-on: ubuntu-latest permissions: