From bbea5a9be09f2c43c6b0b5eed255f85d12e7db2a Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:19:54 -0400 Subject: [PATCH 1/2] Allow npm build/lint/format in Claude workflows Add npm run build, prettier, and eslint (including :fix variants) to the --allowed-tools allowlist in both the upstream-release-docs and @claude mention workflows, so agents can validate their own changes without hitting sandbox denials. The mention workflow also gains Node + deps via the shared ./.github/actions/setup composite, since it previously had no npm tooling at all. Clarify CLAUDE.md/AGENTS.md: the pre-commit hook silently no-ops in CI, unattended agents, and local environments without npm install, so agents should run prettier:fix and eslint:fix explicitly after editing content. --- .github/workflows/claude.yml | 10 ++++++---- .github/workflows/upstream-release-docs.yml | 4 ++-- AGENTS.md | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 6feff6b9..38b95c01 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -26,10 +26,10 @@ jobs: id-token: write actions: read # Required for Claude to read CI results on PRs steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 1 + # Checkout + Node + deps so Claude can run build/lint/format + # scripts when asked. + - name: Set up repo and dependencies + uses: ./.github/actions/setup - name: Run Claude Code id: claude @@ -38,3 +38,5 @@ jobs: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} additional_permissions: | actions: read + claude_args: | + --allowed-tools "Bash(npm run build:*) Bash(npm run prettier:*) Bash(npm run eslint:*)" diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index 0baf5097..535345fb 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -558,7 +558,7 @@ jobs: claude_args: | --model claude-opus-4-7 --max-turns 1000 - --allowed-tools "Bash(gh:*)" + --allowed-tools "Bash(gh:*) Bash(npm run build:*) Bash(npm run prettier:*) Bash(npm run eslint:*)" prompt: | You are running in GitHub Actions with no interactive user. Follow these steps exactly and do NOT ask clarifying questions -- proceed @@ -752,7 +752,7 @@ jobs: claude_args: | --model claude-opus-4-7 --max-turns 200 - --allowed-tools "Bash(gh:*)" + --allowed-tools "Bash(gh:*) Bash(npm run build:*) Bash(npm run prettier:*) Bash(npm run eslint:*)" prompt: | You are running in GitHub Actions with no interactive user. Follow these steps exactly and do NOT ask clarifying questions -- proceed diff --git a/AGENTS.md b/AGENTS.md index adc647e8..c8d805e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,9 +55,9 @@ The project uses automated tooling to enforce code quality and formatting standa - **Pre-commit hooks**: lint-staged runs automatically on `git commit`, applying Prettier and appropriate linters to staged files. - **GitHub Actions**: All PRs trigger automated checks (ESLint, Prettier). -- **No manual formatting needed**: The pre-commit hook handles formatting automatically - you do not need to run formatters manually. +- **Don't rely on the pre-commit hook**: lint-staged only fires on `git commit` when Node, dependencies, and husky are all set up. It silently no-ops in CI and unattended contexts (GitHub Actions, scheduled agents) and in local environments where `npm install` hasn't been run. If you edited content, run `npm run prettier:fix` and `npm run eslint:fix` yourself to avoid lint failures on PR CI. -File type to linter mapping (handled automatically by pre-commit hooks): +File type to linter mapping (run manually if the pre-commit hook doesn't fire): - `.md` files: Prettier only - `.mdx` files: Prettier + ESLint From 663249f3518a57c9ff6941ef8ee6d04c6ad6c730 Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:31:22 -0400 Subject: [PATCH 2/2] Fix claude.yml: add checkout, gate on author_association The initial version of this workflow referenced `./.github/actions/setup` without a prior checkout, so GitHub couldn't resolve the local action. Add a sparse-checkout of `.github` just for action resolution; the composite does its own full checkout + Node + deps. Also gate the job on `author_association` per Copilot's review. Without this, an @claude mention on a fork PR that modified package.json scripts would run attacker-controlled code under our secrets and write permissions. Only OWNER/MEMBER/COLLABORATOR can trigger the workflow. --- .github/workflows/claude.yml | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 38b95c01..6f310c55 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -12,11 +12,23 @@ on: jobs: claude: + # Only run for trusted actors. Fork PRs can modify package.json + # scripts; without this gate, an @claude mention on such a PR + # would execute attacker-controlled code under our secrets and + # write permissions. 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')) + (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(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association)) runs-on: ubuntu-latest timeout-minutes: 20 permissions: @@ -26,6 +38,16 @@ jobs: id-token: write actions: read # Required for Claude to read CI results on PRs steps: + # Initial checkout so GitHub Actions can resolve the local + # composite action below. The composite re-checks out the + # repo itself; this first checkout only needs the .github + # directory. + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 1 + sparse-checkout: .github + # Checkout + Node + deps so Claude can run build/lint/format # scripts when asked. - name: Set up repo and dependencies