From 79fe953469d481a0775b173bb38b51b795083a44 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 29 Oct 2026 00:57:05 -0400 Subject: [PATCH] Remove the @claude mention workflow (claude.yml) The maintainers do not use it, nearly every run it starts is a skip that clutters the Actions tab and PR checks, and it held ANTHROPIC_API_KEY. Delete the workflow and its trusted-author test, and drop the references in AGENTS.md (tree entry, Credentialed workflow jobs), verify-action-pins' header and sdk-watch.yml's secret comment. The historical design record in docs/agent-guidance-inception.md is left as written. Co-Authored-By: Claude Opus 5.5 Signed-off-by: cliffhall --- .github/workflows/claude.yml | 65 ------------- .github/workflows/sdk-watch.yml | 2 +- AGENTS.md | 12 +-- scripts/claude-workflow.test.mjs | 153 ------------------------------- scripts/verify-action-pins.mjs | 5 +- 5 files changed, 6 insertions(+), 231 deletions(-) delete mode 100644 .github/workflows/claude.yml delete mode 100644 scripts/claude-workflow.test.mjs diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index f37a77662c..0000000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Claude Code - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - issues: - types: [opened, assigned] - pull_request_review: - types: [submitted] - -jobs: - # This job holds a credential twice over (`id-token: write`, and the - # `ANTHROPIC_API_KEY` secret), so every action in it is pinned to a commit - # SHA with the release it came from in a trailing comment. A moving tag - # here would let whoever can move it run code next to both. - # `verify:action-pins` enforces it. - # - # It runs only for a trusted author (#4795). A mention is not enough: the - # repository is public, so anyone could otherwise start an agent that holds - # a shell and the API key, and steer it with the text of their comment. - # Each event checks the author of the text that carries the mention (the - # comment, the review, or the issue), never someone further up the thread, - # and only OWNER, MEMBER and COLLABORATOR count. An outside contributor's - # issue that mentions @claude starts nothing; a maintainer's comment on it - # does. `scripts/claude-workflow.test.mjs` evaluates this condition against - # each event, so a disjunct that loses its check fails the script tests. - claude: - timeout-minutes: 30 - 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: read - pull-requests: read - issues: read - id-token: write - actions: read - steps: - - name: Checkout repository - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - with: - fetch-depth: 1 - - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@12dd8d74c712f5f3669365b2369b558c495b1104 # v1.0.238 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - - # Allow Claude to read CI results on PRs - additional_permissions: | - actions: read - - # Trigger when assigned to an issue - assignee_trigger: "claude" - - claude_args: | - --mcp-config .mcp.json - --allowedTools "Bash,mcp__mcp-docs,WebFetch" - --append-system-prompt "If posting a comment to GitHub, give a concise summary of the comment at the top and put all the details in a
block. When working on MCP-related code or reviewing MCP-related changes, use the mcp-docs MCP server to look up the latest protocol documentation. For schema details, reference https://github.com/modelcontextprotocol/modelcontextprotocol/tree/main/schema which contains versioned schemas in JSON (schema.json) and TypeScript (schema.ts) formats." diff --git a/.github/workflows/sdk-watch.yml b/.github/workflows/sdk-watch.yml index f0497ea490..7d9850cf71 100644 --- a/.github/workflows/sdk-watch.yml +++ b/.github/workflows/sdk-watch.yml @@ -31,7 +31,7 @@ # Tokens: `GITHUB_TOKEN` files and comments (`issues: write`). It cannot write # the org project board, so the issue arrives labeled and milestoned and # `issue-triage` boards it; boarding from here waits on a GitHub App (#5061). -# `ANTHROPIC_API_KEY` is the organization secret `claude.yml` already uses. +# `ANTHROPIC_API_KEY` is an organization secret. name: SDK Watch on: diff --git a/AGENTS.md b/AGENTS.md index 8fe94f01a7..a7d231083e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,7 +63,7 @@ servers/ │ overview of how the rules, skills, gates and sweeps fit together ├── .github/workflows/ typescript.yml, python.yml (per-package CI), release.yml (publishes on a GitHub Release), │ version-packages.yml (the changesets PR), prepare-python-release.yml (the CalVer PR), -│ claude.yml (@claude mentions), everything-mcp-diff.yml (everything's interface diff on PRs), +│ everything-mcp-diff.yml (everything's interface diff on PRs), │ main-source.yml (a PR into main must bring nothing that is not on v2/main), │ and the scheduled sweeps that file issues: dependency-refresh.yml, dependabot-alerts.yml, sdk-watch.yml ├── .github/ISSUE_TEMPLATE/ Bug and feature issue forms; config.yml routes security and new servers away @@ -171,8 +171,8 @@ diagnosing a red stage is the `pre-push-gate` skill. A workflow job **holds a credential** when it can mint an OIDC token or push a package (`id-token: write`, `packages: write`), or is handed any secret other -than `GITHUB_TOKEN`. Today that is `release.yml`'s publish jobs, `claude.yml`, -and `sdk-watch.yml`'s `analyze` and `post` jobs (`ANTHROPIC_API_KEY`). Jobs +than `GITHUB_TOKEN`. Today that is `release.yml`'s publish jobs and +`sdk-watch.yml`'s `analyze` and `post` jobs (`ANTHROPIC_API_KEY`). Jobs that hold none can still need pinning under the rules below: `release.yml`'s build jobs and `sdk-watch.yml`'s `sweep`, whose outputs `analyze` reads. @@ -184,12 +184,6 @@ build jobs and `sdk-watch.yml`'s `sweep`, whose outputs `analyze` reads. downloads**, since what it builds is what gets published, and for **a job whose outputs a credentialed job reads**, since the reader acts on them. `verify:action-pins` enforces both. Other jobs keep moving major tags. -- **A credentialed job that a comment, review or issue can start runs only - for a trusted author**: `OWNER`, `MEMBER` or `COLLABORATOR` in the - `author_association` of the text that triggered it, never of the thread - around it. Today that is `claude.yml`, whose `if:` checks it on every event - it listens to; `scripts/claude-workflow.test.mjs` evaluates that condition - per event and fails when one loses its check. - **A job that holds a publish credential does not install dependencies or build.** `release.yml` builds, tests and verifies each package in a job with no `id-token`, and the publish job only downloads that artifact and diff --git a/scripts/claude-workflow.test.mjs b/scripts/claude-workflow.test.mjs deleted file mode 100644 index d1f502c6ae..0000000000 --- a/scripts/claude-workflow.test.mjs +++ /dev/null @@ -1,153 +0,0 @@ -// Tests for the trusted-author gate on `.github/workflows/claude.yml` (#4795). -// The `claude` job holds a shell, the API key and `id-token: write`, and it -// reads the text that mentions it, so who may start it is a security -// property. These tests evaluate the job's `if:` against a synthetic payload -// for every event the workflow listens to, rather than matching its text, so -// a disjunct that loses its author check, or checks the wrong author, fails -// here. Run via `npm run test:scripts`. -// -// The evaluator covers only the expression subset the condition uses -// (`==`, `&&`, `||`, `!`, parentheses, string literals, `github.*` paths, -// `contains` and `fromJSON`), with `contains` case-insensitive as in Actions. -// It refuses any other token, so widening the condition past that subset -// fails loudly instead of being evaluated wrongly. - -import { test } from "node:test"; -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { parse } from "yaml"; - -const repoRoot = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - "..", -); -const workflow = parse( - readFileSync(path.join(repoRoot, ".github/workflows/claude.yml"), "utf8"), -); -const condition = workflow.jobs.claude.if; - -const TOKEN = - /\s+|'(?:[^']|'')*'|==|&&|\|\||!|\(|\)|,|contains|fromJSON|github(?:\.[A-Za-z_][A-Za-z0-9_-]*)+/y; - -/** Compiles the expression subset above into a function of `github`. */ -function compile(expr) { - let js = ""; - TOKEN.lastIndex = 0; - while (TOKEN.lastIndex < expr.length) { - const at = TOKEN.lastIndex; - const match = TOKEN.exec(expr); - if (!match) { - throw new Error(`unsupported expression syntax at: ${expr.slice(at)}`); - } - const tok = match[0]; - if (tok.startsWith("'")) { - js += JSON.stringify(tok.slice(1, -1).replaceAll("''", "'")); - } else if (tok.startsWith("github.")) { - // A missing property reads as null, as it does in Actions. - js += `(${tok.replaceAll(".", "?.")} ?? null)`; - } else if (tok === "==") { - // Only `event_name` is compared, against exact event names. - js += "==="; - } else { - js += tok; - } - } - return new Function("github", "contains", "fromJSON", `return (${js});`); -} - -const fold = (v) => (typeof v === "string" ? v.toLowerCase() : v); -const eq = (a, b) => fold(a) === fold(b); -const contains = (haystack, needle) => - Array.isArray(haystack) - ? haystack.some((item) => eq(item, needle)) - : haystack != null && - String(haystack).toLowerCase().includes(String(needle).toLowerCase()); - -const evaluate = (github) => - Boolean(compile(condition)(github, contains, JSON.parse)); - -// One payload per event the workflow listens to, with the mention placed in -// the text that event carries, and `who` as that text's author association. -// The surrounding issue or pull request is always opened by a maintainer, so -// a gate that read the wrong author would let an outsider through. -const EVENTS = { - issue_comment: (who) => ({ - event: { - issue: { body: "", title: "", author_association: "OWNER" }, - comment: { body: "@claude review this", author_association: who }, - }, - }), - pull_request_review_comment: (who) => ({ - event: { - pull_request: { author_association: "OWNER" }, - comment: { body: "@claude fix this line", author_association: who }, - }, - }), - pull_request_review: (who) => ({ - event: { - pull_request: { author_association: "OWNER" }, - review: { body: "@claude summarize", author_association: who }, - }, - }), - issues: (who) => ({ - event: { - issue: { - body: "@claude look at this", - title: "", - author_association: who, - }, - }, - }), -}; - -const TRUSTED = ["OWNER", "MEMBER", "COLLABORATOR"]; -const UNTRUSTED = [ - "CONTRIBUTOR", - "FIRST_TIME_CONTRIBUTOR", - "FIRST_TIMER", - "MANNEQUIN", - "NONE", -]; - -test("every event the workflow listens to has a payload here", () => { - assert.deepEqual(Object.keys(workflow.on).sort(), Object.keys(EVENTS).sort()); -}); - -for (const [name, payload] of Object.entries(EVENTS)) { - const run = (who) => evaluate({ event_name: name, ...payload(who) }); - - test(`${name}: a trusted author's mention starts the job`, () => { - for (const who of TRUSTED) assert.equal(run(who), true, who); - }); - - test(`${name}: an untrusted author's mention starts nothing`, () => { - for (const who of UNTRUSTED) assert.equal(run(who), false, who); - }); - - test(`${name}: a trusted author without a mention starts nothing`, () => { - const github = { event_name: name, ...payload("OWNER") }; - const text = - github.event.comment ?? github.event.review ?? github.event.issue; - text.body = "no mention here"; - assert.equal(evaluate(github), false); - }); -} - -test("an outsider's issue title mention starts nothing", () => { - const github = { - event_name: "issues", - event: { - issue: { body: "", title: "@claude do it", author_association: "NONE" }, - }, - }; - assert.equal(evaluate(github), false); - github.event.issue.author_association = "MEMBER"; - assert.equal(evaluate(github), true); -}); - -test("the evaluator refuses syntax it does not model", () => { - assert.throws(() => compile("github.event_name != 'x'"), /unsupported/); - assert.throws(() => compile("startsWith(github.ref, 'x')"), /unsupported/); -}); diff --git a/scripts/verify-action-pins.mjs b/scripts/verify-action-pins.mjs index 344dd00953..da588c3f5d 100644 --- a/scripts/verify-action-pins.mjs +++ b/scripts/verify-action-pins.mjs @@ -18,14 +18,13 @@ // // 1. can mint an OIDC token or push a package: `id-token: write` or // `packages: write` (or `write-all`), in its own `permissions:` or, absent -// one, the workflow's. Here that is `release.yml`'s two publish jobs and -// `claude.yml`; +// one, the workflow's. Here that is `release.yml`'s two publish jobs; // 2. is handed any secret other than `GITHUB_TOKEN`, its own or through the // workflow-level `env:`, in any spelling of the expression (`secrets.X`, // `secrets['X']`, `secrets[matrix.name]`), or as a reusable-workflow // call's `secrets: inherit` (a `secrets:` mapping is read like any other // expression, so one passing only `GITHUB_TOKEN` does not count). Here -// that is `claude.yml` again, for `ANTHROPIC_API_KEY`; or +// that is `sdk-watch.yml`'s `analyze` and `post`, for `ANTHROPIC_API_KEY`; or // 3. uploads an artifact that a credentialed job downloads and has anywhere // in its transitive `needs` (an artifact is scoped to the run, so a job // can download from any job that finished before it), so every producer