Skip to content

Authenticate CI Claude review with an API key in a main-only environment - #968

Merged
seathatflowsinourveins merged 1 commit into
mainfrom
claude/api-actions-ci-api-key-20261010
Oct 10, 2026
Merged

seathatflowsinourveins merged 1 commit into
mainfrom
claude/api-actions-ci-api-key-20261010

Conversation

@seathatflowsinourveins

Copy link
Copy Markdown
Owner

Scope

  • What this PR changes: CI Claude review (claude-pr-review.yml, claude-pr-toolkit-review.yml, harness-audit.yml) authenticates with an Anthropic API key, the environment secret ANTHROPIC_API_KEY of the main-only claude-review environment, instead of workload identity federation. This follows the owner's ruling of 2026-10-10 (about 13:44Z, relayed by the command center, paraphrased): use the keys; federation stays unconfigured. Its acceptance dispatch, run 38056354892, was refused like the runs of 10-08.
  • Base commit: e1c88ff3d (main after Fence the headless /skill-doctor run #925).
  • Lane: lane:foundation.
  • Owned paths touched: the three workflows; tests/test_workflow_hardening.py, tests/test_workflow_policy.py, tests/test_claude_pr_review_workflow.py, tests/test_claude_pr_toolkit_review_workflow.py, tests/test_claude_harness_audit_bounds.py, tests/test_claude_federation_diagnostics.py; four decision records; docs/github-automation.md; manifests/evidence.json.

What changed:

  • Each workflow passes anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} and no federation input.
  • The job that uses the key declares environment: claude-review, and no job holds id-token: write.
  • All three pass github_token, so the pinned action requests no GitHub OIDC token.
  • Unchanged: dispatch and schedule on main only, the owner and first-attempt guards, the bounds step, the daily ceiling, the read-only tools, the job summary as the only output, and Retain Claude federation diagnostics and pinned vendor documentation #961's error_class logic.

The secret:

  • ANTHROPIC_API_KEY holds the inventory entry anthropic-api-3, the cold spare, so CI spend stays separate from the local worker's api-4.
  • The command center set it on the owner's explicit OK, through credential_run.py, with the value on stdin only. It was first a repository secret (2026-10-10T13:49:43Z), then an environment secret of claude-review (2026-10-10T13:55:05Z); the repository copy was deleted.
  • Read back by me at 14:11Z: the environment's deployment branch policy allows main only, the environment holds the secret, and the repository has no secret of that name.

Policy and tests:

  • The federation exemption and the three id-token: write write grants leave tests/test_workflow_policy.py.
  • New tests.test_workflow_hardening.ClaudeApiKeyAuthTests requires:
    • the secret once per workflow;
    • the key-using job in the claude-review environment, and no other job in one;
    • no federation input, no id-token and no pull request trigger;
    • the secret in no other workflow.
  • It fails on main's workflows (6 failures before the environment requirement was added, 3 for the environment test alone) and passes here.
  • Retain Claude federation diagnostics and pinned vendor documentation #961's federation-diagnostics record is marked superseded for CI review. Its vendor snapshots stay retained and tested, and its two workflow-header tests now require the API-key environment and no federation claim.

SOTA sources

  • Claude Code GitHub Actions, https://docs.claude.com/en/docs/claude-code/github-actions (read 2026-10-10T13:48Z):
    • the anthropic_api_key input with ${{ secrets.ANTHROPIC_API_KEY }};
    • workload identity federation as the alternative to a stored key;
    • id-token: write needed for the default GitHub App authentication and for the federation exchange.
  • anthropics/claude-code-action at 2dca132ff0e0c4094ce6048b422c6915a071210b (v1.0.247):
    • action.yml:67-89 (the anthropic_api_key, federation and github_token inputs);
    • src/github/token.ts:160-168 (no OIDC request when github_token is given);
    • base-action/src/workload-identity.ts:43-47 (federation only when its inputs are set).
  • GitHub Docs, Managing environments for deployment (read 2026-10-10T14:35Z; a job that references an environment must follow its protection rules, deployment branches included, before running or accessing its secrets): https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment.
  • zizmor 1.30.1 secrets-outside-env audit, auditor persona: on main it reports six findings in these three files; here, none.

Evidence-class table

Claim Evidence class Command / receipt
The three workflows use the key from the claude-review environment, carry no federation input, hold no id-token, and have no pull request trigger; no other workflow names the secret synthetic tests.test_workflow_hardening.ClaudeApiKeyAuthTests (fails on main's workflows, passes here)
With github_token passed, the pinned action requests no OIDC token source_review claude-code-action@2dca132f, files above
The environment allows main only and holds the secret; the repository has no secret of that name native_proven gh api repos/…/environments/claude-review and its deployment-branch-policies; gh secret list --env claude-review; gh secret list (names and times only, 14:11Z)
A review authenticates and runs on the key — not yet run: the command center's acceptance dispatch after landing (a successful review, total_cost_usd > 0, non-empty modelUsage)

Local commands run

$ python3 -m unittest <the 65 test modules that name these workflows, federation, the policy or manifests/evidence.json>   # TMPDIR on /var/tmp, at the rebased head
Ran 4266 tests, OK (skipped=44)
$ python3 scripts/validate.py
exit 0 (70 components, 11404 hashed files)
$ actionlint .github/workflows/*.yml
exit 0
$ zizmor 1.30.1 --offline --persona {regular,pedantic,auditor} on the three workflows
No findings
$ gitleaks git --log-opts="$(git merge-base origin/main HEAD)..HEAD" --config .gitleaks.toml --redact
no leaks found

Decision record

docs/decisions/2026-10-08-claude-actions-pr-review.md, "Authentication by API key (the owner's ruling, 2026-10-10)". Short dated notes in 2026-10-04-ci-least-privilege.md (federation exemption withdrawn), 2026-10-08-claude-actions-harness-audit-bounds.md, 2026-10-09-claude-actions-pr-toolkit-review.md and 2026-10-10-claude-federation-diagnostics.md.

🤖 Generated with Claude Code

The owner ruled on 2026-10-10 (relayed by the command center, paraphrased)
that CI Claude review authenticates with an Anthropic API key, not workload
identity federation, which refused every run so far. The key is the
environment secret ANTHROPIC_API_KEY of `claude-review`, whose deployment
branch policy allows main only, so a workflow pushed on another branch gets
no key.

claude-pr-review.yml, claude-pr-toolkit-review.yml and harness-audit.yml
pass anthropic_api_key from that secret and no federation input. The job
that uses the key declares environment: claude-review, and no job holds
id-token: write. They pass github_token, so the pinned action (2dca132f)
requests no GitHub OIDC token (src/github/token.ts:160-168,
base-action/src/workload-identity.ts:43-47). Every other guard is unchanged:
dispatch and schedule on main only, the owner and first-attempt guards, the
bounds step, the daily ceiling and the read-only tools.

The federation exemption and the three id-token write grants leave
tests/test_workflow_policy.py. The new
tests.test_workflow_hardening.ClaudeApiKeyAuthTests requires the secret once
per workflow, the key-using job in the environment, no federation input, no
id-token, no pull request trigger, and the secret in no other workflow. It
fails on main's workflows and passes here. The decision records and
docs/github-automation.md carry the ruling, paraphrased. #961's federation
diagnostics record is marked superseded for CI review; its vendor snapshots
stay retained and tested, and its two workflow-header tests now require the
API-key environment and no federation claim. #961's error_class logic in the
three workflows is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@seathatflowsinourveins seathatflowsinourveins added the lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers label Oct 10, 2026
@socket-security

Copy link
Copy Markdown

Dependency limit exceeded — report not shown.

This pull request scan exceeded the 10,000-dependency limit applied to this scan, so the results are incomplete and may be inaccurate. To avoid reporting false positives, Socket has not posted a report.

Upgrade your plan to raise the dependency limit and get complete reports, or view the partial scan in the dashboard.

Socket is always free for open source. If this is a non-commercial open source project, contact us to request a free Team account.

@seathatflowsinourveins
seathatflowsinourveins marked this pull request as ready for review October 10, 2026 16:32
@seathatflowsinourveins
seathatflowsinourveins merged commit 9d4c00c into main Oct 10, 2026
36 checks passed
@seathatflowsinourveins
seathatflowsinourveins deleted the claude/api-actions-ci-api-key-20261010 branch October 10, 2026 16:44
seathatflowsinourveins added a commit that referenced this pull request Oct 10, 2026
The record's inverse named a SHA that does not exist; #968's merge commit is
9d4c00c (git rev-parse 9d4c00c). The
federation note now says the history of those workflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seathatflowsinourveins added a commit that referenced this pull request Oct 10, 2026
The record's inverse named a SHA that does not exist; #968's merge commit is
9d4c00c (git rev-parse 9d4c00c). The
federation note now says the history of those workflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seathatflowsinourveins added a commit that referenced this pull request Oct 10, 2026
The record's inverse named a SHA that does not exist; #968's merge commit is
9d4c00c (git rev-parse 9d4c00c). The
federation note now says the history of those workflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seathatflowsinourveins added a commit that referenced this pull request Oct 10, 2026
The record's inverse named a SHA that does not exist; #968's merge commit is
9d4c00c (git rev-parse 9d4c00c). The
federation note now says the history of those workflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seathatflowsinourveins added a commit that referenced this pull request Oct 11, 2026
… git

The GPT read at 13d1eb7 asked, under the correction rule, for:
- the upstream sources of the environment reading: GitHub's workflow syntax
  for jobs.<job_id>.environment (a name, or an object with name and url) and
  YAML 1.2.2's block and flow mappings, quoted keys and comments, cited with
  page digests in the record and next to the regression cases;
- a regression for the corrected inverse pin: RetirementInversePinTests reads
  the pin from the record and checks it is #968's merge commit holding the
  three workflows. It fails on the former SHA (no such commit) and is skipped,
  with the reason, only outside a worktree or in a shallow clone (CI clones
  with fetch-depth 0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seathatflowsinourveins added a commit that referenced this pull request Oct 11, 2026
The record's inverse named a SHA that does not exist; #968's merge commit is
9d4c00c (git rev-parse 9d4c00c). The
federation note now says the history of those workflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seathatflowsinourveins added a commit that referenced this pull request Oct 11, 2026
… git

The GPT read at 13d1eb7 asked, under the correction rule, for:
- the upstream sources of the environment reading: GitHub's workflow syntax
  for jobs.<job_id>.environment (a name, or an object with name and url) and
  YAML 1.2.2's block and flow mappings, quoted keys and comments, cited with
  page digests in the record and next to the regression cases;
- a regression for the corrected inverse pin: RetirementInversePinTests reads
  the pin from the record and checks it is #968's merge commit holding the
  three workflows. It fails on the former SHA (no such commit) and is skipped,
  with the reason, only outside a worktree or in a shallow clone (CI clones
  with fetch-depth 0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seathatflowsinourveins added a commit that referenced this pull request Oct 11, 2026
The record's inverse named a SHA that does not exist; #968's merge commit is
9d4c00c (git rev-parse 9d4c00c). The
federation note now says the history of those workflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seathatflowsinourveins added a commit that referenced this pull request Oct 11, 2026
… git

The GPT read at 13d1eb7 asked, under the correction rule, for:
- the upstream sources of the environment reading: GitHub's workflow syntax
  for jobs.<job_id>.environment (a name, or an object with name and url) and
  YAML 1.2.2's block and flow mappings, quoted keys and comments, cited with
  page digests in the record and next to the regression cases;
- a regression for the corrected inverse pin: RetirementInversePinTests reads
  the pin from the record and checks it is #968's merge commit holding the
  three workflows. It fails on the former SHA (no such commit) and is skipped,
  with the reason, only outside a worktree or in a shallow clone (CI clones
  with fetch-depth 0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seathatflowsinourveins added a commit that referenced this pull request Oct 11, 2026
* Retire Claude review inside GitHub; AI reviews run locally

The owner decided on 2026-10-10 (about 17:15Z, relayed by the command center)
that Claude review no longer runs inside GitHub: AI reviews run on the local
host and GitHub enforces the gates. The API refused every request that carried
claude-code-action's attribution (CLAUDE_CODE_ENTRYPOINT=claude-code-github-action,
base-action/src/parse-sdk-options.ts:284 at 2dca132f) on all five Console keys,
while the same keys work from the CLI and the Agent SDK (the command center's
correction #141, measured).

Removed: claude-pr-review.yml, claude-pr-toolkit-review.yml, harness-audit.yml
and their three test modules, their zizmor-coverage entries and their
local-pages inventory paths. test_claude_federation_diagnostics keeps its
retained-snapshot and decision-claim tests. A new
InGitHubClaudeReviewRetiredTests keeps the three files absent and no workflow
running the Claude action or naming an Anthropic key.

New record docs/decisions/2026-10-10-retire-in-github-claude-review.md (cause,
what remains, revisit triggers, inverse); supersession notes in the four
claude-actions/federation records and the decision-record index;
docs/github-automation.md's current practice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Pin the repository grant count at 90 after the retirement

The three removed workflows were in the local-pages repository grants. At the
landing base, main f38116f (93 grants), the test's own derivation
(grants.inventory_paths over git ls-files) gives 90 for this branch, equal to
the policy's repository grants, and the test pins 90. At the earlier bases
eb31d23 and 9d4c00c the same derivation gave 90 and 89.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Take up the command center's notes on the retirement

- .github/actions-permissions.json drops anthropics/claude-code-action@* and
  oven-sh/setup-bun@*: no workflow uses them; a new test keeps every allowed
  pattern in use, and the record's inverse puts them back.
- docs/github-automation.md: the allow-list text, no "harness description",
  no "still awaits hosted acceptance" clause.
- The retirement test catches the claude-review environment in its plain,
  name: and quoted forms, and reads *.yaml as well.
- Record nits: #953 marked open, the check is named sota-sources, and the
  federation-diagnostics note says the three workflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cite #968's real merge commit in the inverse

The record's inverse named a SHA that does not exist; #968's merge commit is
9d4c00c (git rev-parse 9d4c00c). The
federation note now says the history of those workflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Read each job's environment in the retirement guard

The GPT read at 48e4bd3 found three valid YAML forms of the claude-review
environment that the regex guard let through: a flow mapping, a quoted name
key, and a comment after the key. The guard now reads each job's environment
as a scalar or a mapping's name (job_environments), cross-checked against
PyYAML when it is installed; all three forms are regressions, each caught here
and missed at e46f68e, and actionlint accepts each fixture.

The record names the claude-native-practice skill rows as a dated follow-up
for cc-native-practice (the command center's ruling at 20:19Z).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Word the skill-row follow-up without the pinned-copy premise

The command center's correction #146: no personal copy of claude-native-practice
is installed on this host; sessions load the checkout's project skill. The
follow-up is now stated neutrally: the skill rows for the retired route are
updated in a cc-native-practice pull request, then re-pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* State what the environment reading covers, and what PyYAML covers

The docstring claimed comments in general; the text reading handles a comment
after the key, not a comment line before name: at the job's indentation or a
multi-line flow mapping. The PyYAML comparison covers those, and CI runs it
(validate run 38084944806, passed, not skipped). The command center's micro at
08a76ab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cite the environment forms' sources, and test the inverse pin against git

The GPT read at 13d1eb7 asked, under the correction rule, for:
- the upstream sources of the environment reading: GitHub's workflow syntax
  for jobs.<job_id>.environment (a name, or an object with name and url) and
  YAML 1.2.2's block and flow mappings, quoted keys and comments, cited with
  page digests in the record and next to the regression cases;
- a regression for the corrected inverse pin: RetirementInversePinTests reads
  the pin from the record and checks it is #968's merge commit holding the
  three workflows. It fails on the former SHA (no such commit) and is skipped,
  with the reason, only outside a worktree or in a shallow clone (CI clones
  with fetch-depth 0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant