From 9b4c83250b7331402892a6af1429dd3ac9440ff8 Mon Sep 17 00:00:00 2001 From: William Grzybowski Date: Fri, 14 Aug 2026 14:38:31 -0300 Subject: [PATCH 1/3] Accept a Claude Code OAuth token as the auth secret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit claude-code-action v1.0.187 takes claude_code_oauth_token as a first-class alternative to anthropic_api_key, so subscription-billed repos need no API key — and no self-hosted runner or custom action. Either secret satisfies the workflow; a job with neither fails before the checkout instead of deep inside the review step. --- .github/workflows/claude-review.yml | 29 +++++++++++++++++++++++++++-- README.md | 7 ++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index c9c10d8..7e094dc 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -40,6 +40,8 @@ name: Claude Review (shared) # pull-requests: write # secrets: # anthropic-api-key: ${{ secrets.CLAUDE_API_KEY }} +# # or, on subscription auth instead of API billing: +# claude-code-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # # The job fails on a blocking finding. Whether that stops a merge is branch # protection's decision, made per repo, and reversible without touching this. @@ -88,8 +90,16 @@ on: default: 'master' secrets: anthropic-api-key: - description: 'Anthropic API key. Mapped by the caller, since the secret name differs per repo.' - required: true + description: >- + Anthropic API key. Provide this or claude-code-oauth-token; a step + fails the job when both are missing. Mapped by the caller, since the + secret name differs per repo. + required: false + claude-code-oauth-token: + description: >- + Claude Code OAuth token (from `claude setup-token`) — the + subscription alternative to an API key. + required: false # One review per PR. Rapid pushes previously started overlapping reviews that # raced to overwrite the same sticky comment, and paid for every superseded run. @@ -178,6 +188,18 @@ jobs: echo "required status check, which would report an unreviewed PR as reviewed." exit 1 + # Fail before spending a runner minute on a checkout: with neither + # secret the review step errors anyway, only later and less clearly. + - name: Check an auth secret is present + env: + API_KEY: ${{ secrets.anthropic-api-key }} + OAUTH_TOKEN: ${{ secrets.claude-code-oauth-token }} + run: | + if [ -z "$API_KEY" ] && [ -z "$OAUTH_TOKEN" ]; then + echo "::error::pass anthropic-api-key or claude-code-oauth-token; neither is set." + exit 1 + fi + - name: Checkout repository uses: actions/checkout@v4 with: @@ -272,7 +294,10 @@ jobs: id: review uses: anthropics/claude-code-action@v1.0.187 with: + # Both pass through as env vars; the action treats an empty one as + # absent and requires at least one. Callers should set exactly one. anthropic_api_key: ${{ secrets.anthropic-api-key }} + claude_code_oauth_token: ${{ secrets.claude-code-oauth-token }} # Passed so the action uses this token directly instead of exchanging # its OIDC token for an Anthropic GitHub App token. `setupGitHubToken` # returns early on it, and the exchange is the *only* thing that runs diff --git a/README.md b/README.md index f48ce26..1fb7480 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,8 @@ jobs: pull-requests: write secrets: anthropic-api-key: ${{ secrets.CLAUDE_API_KEY }} + # or, on subscription auth instead of API billing: + claude-code-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} ``` No `id-token: write`: nothing here mints an OIDC token, because the workflow @@ -197,7 +199,10 @@ either, so granting it in a caller has no effect on the token the job runs with. | `tooling-ref` | `master` | Ref this repo's `review/` assets come from; see below | The secret is named, not inherited, because the repos call it different things -(`CLAUDE_API_KEY` vs `CLAUDE_TOKEN`). The `anthropics/claude-code-action` +(`CLAUDE_API_KEY` vs `CLAUDE_TOKEN`). Auth is one of two secrets: an Anthropic +API key, or a Claude Code OAuth token (from `claude setup-token`) for +subscription billing. Set exactly one; a job with neither fails before the +checkout. The `anthropics/claude-code-action` version is hardcoded rather than an input: `uses:` does not evaluate expressions, and a configurable version is how the consumers ended up on v1.0.182, v1.0.154 and v1.0.134 in the first place. Bump it here and every From 09709439004a4f38b10cd717fd3a7412380d0fdd Mon Sep 17 00:00:00 2001 From: William Grzybowski Date: Fri, 14 Aug 2026 14:50:01 -0300 Subject: [PATCH 2/3] Enforce exactly one auth secret, and fix the comment the guard staled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings: the fork-PR comment in claude-review-self.yml named the review step as where an empty secret dies, but the new guard fails first; and "set exactly one" was advice with nothing behind it while both usage snippets invite copying both lines. The guard now fails on both-set too — the two secrets bill different accounts, so which wins must not be an undocumented pick. --- .github/workflows/claude-review-self.yml | 5 +++-- .github/workflows/claude-review.yml | 11 +++++++++-- README.md | 5 +++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/claude-review-self.yml b/.github/workflows/claude-review-self.yml index dc302d8..f3f1ca1 100644 --- a/.github/workflows/claude-review-self.yml +++ b/.github/workflows/claude-review-self.yml @@ -37,8 +37,9 @@ name: Claude Review (self) # On a pull request from a fork this degrades rather than misbehaves. The merge # commit is a commit in this repository, so the tooling checkout resolves; the # workflow being run is still the fork's code merged into `master`, but -# `pull_request` gives a fork run no secrets, so `anthropic-api-key` arrives -# empty and the review step fails — it cannot spend tokens. +# `pull_request` gives a fork run no secrets, so both auth secrets arrive +# empty and the job fails its auth-secret check before the review runs — it +# cannot spend tokens. # `require-write-access` is left at its default `true`, which stops a # non-writer's pull request before that. diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 7e094dc..90d2ae8 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -190,7 +190,9 @@ jobs: # Fail before spending a runner minute on a checkout: with neither # secret the review step errors anyway, only later and less clearly. - - name: Check an auth secret is present + # Both is also an error — which credential would win is the CLI's + # undocumented pick, and the two bill different accounts. + - name: Check exactly one auth secret is present env: API_KEY: ${{ secrets.anthropic-api-key }} OAUTH_TOKEN: ${{ secrets.claude-code-oauth-token }} @@ -199,6 +201,11 @@ jobs: echo "::error::pass anthropic-api-key or claude-code-oauth-token; neither is set." exit 1 fi + if [ -n "$API_KEY" ] && [ -n "$OAUTH_TOKEN" ]; then + echo "::error::pass only one of anthropic-api-key and claude-code-oauth-token; both are set," + echo "::error::and they bill different accounts — which one wins would be undocumented." + exit 1 + fi - name: Checkout repository uses: actions/checkout@v4 @@ -295,7 +302,7 @@ jobs: uses: anthropics/claude-code-action@v1.0.187 with: # Both pass through as env vars; the action treats an empty one as - # absent and requires at least one. Callers should set exactly one. + # absent. The guard step above enforces exactly one is set. anthropic_api_key: ${{ secrets.anthropic-api-key }} claude_code_oauth_token: ${{ secrets.claude-code-oauth-token }} # Passed so the action uses this token directly instead of exchanging diff --git a/README.md b/README.md index 1fb7480..e8a2ba3 100644 --- a/README.md +++ b/README.md @@ -201,8 +201,9 @@ either, so granting it in a caller has no effect on the token the job runs with. The secret is named, not inherited, because the repos call it different things (`CLAUDE_API_KEY` vs `CLAUDE_TOKEN`). Auth is one of two secrets: an Anthropic API key, or a Claude Code OAuth token (from `claude setup-token`) for -subscription billing. Set exactly one; a job with neither fails before the -checkout. The `anthropics/claude-code-action` +subscription billing. Set exactly one — a job with neither, or with both, +fails before the checkout; the two bill different accounts, so an undocumented +winner is not a choice the workflow will make silently. The `anthropics/claude-code-action` version is hardcoded rather than an input: `uses:` does not evaluate expressions, and a configurable version is how the consumers ended up on v1.0.182, v1.0.154 and v1.0.134 in the first place. Bump it here and every From c95276084136c8efb24954af511826d2b27dd55d Mon Sep 17 00:00:00 2001 From: William Grzybowski Date: Fri, 14 Aug 2026 15:11:13 -0300 Subject: [PATCH 3/3] Stop the usage examples handing callers the rejected configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings: both usage snippets mapped both secrets — live YAML, not an either/or — which is exactly what the guard now fails on, so a migrating caller copying the block would red every review. Comment out the alternative line. Also document the both-set failure in both secret descriptions, and emit the both-set error as one annotation. --- .github/workflows/claude-review.yml | 18 ++++++++++-------- README.md | 5 +++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 90d2ae8..b45eabd 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -40,8 +40,9 @@ name: Claude Review (shared) # pull-requests: write # secrets: # anthropic-api-key: ${{ secrets.CLAUDE_API_KEY }} -# # or, on subscription auth instead of API billing: -# claude-code-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} +# # or, on subscription auth instead of API billing — set exactly one +# # of the two; mapping both fails the job: +# # claude-code-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # # The job fails on a blocking finding. Whether that stops a merge is branch # protection's decision, made per repo, and reversible without touching this. @@ -91,14 +92,15 @@ on: secrets: anthropic-api-key: description: >- - Anthropic API key. Provide this or claude-code-oauth-token; a step - fails the job when both are missing. Mapped by the caller, since the - secret name differs per repo. + Anthropic API key. Set exactly one of this and + claude-code-oauth-token; a step fails the job when both are missing, + and also when both are set. Mapped by the caller, since the secret + name differs per repo. required: false claude-code-oauth-token: description: >- Claude Code OAuth token (from `claude setup-token`) — the - subscription alternative to an API key. + subscription alternative to an API key. Same exactly-one rule. required: false # One review per PR. Rapid pushes previously started overlapping reviews that @@ -202,8 +204,8 @@ jobs: exit 1 fi if [ -n "$API_KEY" ] && [ -n "$OAUTH_TOKEN" ]; then - echo "::error::pass only one of anthropic-api-key and claude-code-oauth-token; both are set," - echo "::error::and they bill different accounts — which one wins would be undocumented." + echo "::error::pass only one of anthropic-api-key and claude-code-oauth-token; both are set." + echo "They bill different accounts, so which one wins would be undocumented." exit 1 fi diff --git a/README.md b/README.md index e8a2ba3..4bb64e1 100644 --- a/README.md +++ b/README.md @@ -179,8 +179,9 @@ jobs: pull-requests: write secrets: anthropic-api-key: ${{ secrets.CLAUDE_API_KEY }} - # or, on subscription auth instead of API billing: - claude-code-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # or, on subscription auth instead of API billing — set exactly one of + # the two; mapping both fails the job: + # claude-code-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} ``` No `id-token: write`: nothing here mints an OIDC token, because the workflow