feat(auth): make claude-api-key optional so the action can run keyless (NEX-2513) - #4
Merged
Merged
Conversation
## Why Every repo in the org is moving off static Anthropic API keys to workload identity federation (NEX-2513). This action is the last consumer that hard-fails without `claude-api-key`, so it alone keeps a static key alive in repo secrets. ## What changes Cherry-picks upstream PR anthropics#116 (@ralphpina), which is open against an upstream `main` that has not moved since 2026-02-11: - `action.yml` — `claude-api-key` becomes `required: false`, and the scan step unsets an empty `ANTHROPIC_API_KEY` rather than exiting 1. An empty-but-set value outranks keyless auth, so unsetting it is what makes federation reachable. - `claude_api_client.py` — construct `Anthropic()` with no key when none is present and let the SDK resolve environment credentials, instead of raising. - `github_action_audit.py` — `validate_claude_available()` accepts a static credential or federation env, with an error message that names both. Passing `claude-api-key` behaves exactly as before; this only adds a fallback path when it is absent. Two fork-local fixes on top of the upstream patch: - the conflict against our cost-metrics change (#3) is resolved by keeping the keyless client construction and our `usage` / `_record_usage` block; - `logger.info("Claude API client initialized successfully")` had ended up as the last statement of `_record_usage` in #3, so it logged on every API response. Moved back into `__init__`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Why Upstream anthropics#116 unblocked ClaudeAPIClient but missed create_findings_filter, which gates Claude filtering on `use_claude_filtering and api_key`. Under federation there is no api_key, so the false-positive filter silently switched off — a keyless scan on nexus-status came back with `"justification": "Claude filtering disabled"` while still reporting findings and exiting 0. That is the worst shape of failure here: the scan looks clean and the PR comment just gets noisier. ## What changes Gate on `use_claude_filtering` alone and let the client resolve the credential. FindingsFilter already validates API access on construction and degrades to hard rules if it fails, so the key check bought nothing that the fallback did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Why The fallback that re-runs the audit without the diff has been unreachable. nexus-status hit it for real: a PR that swept in build artifacts produced a ~1.2M-token prompt against the 1M limit, and instead of retrying without the diff the action reported `Claude Code execution failed with return code 1` and the PR got no review at all. Two independent faults, either one fatal: - The detection sat inside the `returncode == 0` branch. The CLI writes its result envelope to stdout and *still* exits non-zero on an API error, so stdout was never parsed. Worse, the loop treated it as a transient failure and spent all three retries resending the same oversized prompt. - The test was `result == 'Prompt is too long'`, exact equality. The CLI now appends the measured counts — `Prompt is too long · the request is ~1199699 tokens (limit 1000000) …` — so equality could never hold again. This is not the same gap the 406 work closed. That change made an oversized diff *fetchable* by assembling it from per-file patches; it makes the prompt bigger, not smaller. Nothing downstream bounded what that diff cost in tokens. ## What changes - `github_action_audit.py`: parse stdout before branching on the return code, and match the message by prefix. Usage is still recorded exactly once per attempt, so a billed prompt-too-long attempt keeps being counted. - `test_claude_runner.py`: a regression test built from the observed envelope — non-zero exit plus the counts-bearing message — asserting both that the fallback fires and that the oversized prompt is not retried. It fails against the previous code with the production error string. - Same file: the `validate_claude_available` assertion still expected the key-only message that upstream anthropics#116 replaced. Updated to the message that names both credential routes; the suite was 180/1 and is now 181/0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Every repo in the org is moving off static Anthropic API keys to workload identity federation (NEX-2513 step 2). This action is the last consumer that hard-fails without
claude-api-key, so it alone keeps a static key alive in repo secrets.What changes
Cherry-picks upstream anthropics/claude-code-security-review#116 by @ralphpina — open against an upstream
mainthat has not moved since 2026-02-11, so we carry it here.action.yml—claude-api-keybecomesrequired: false, and the scan step unsets an emptyANTHROPIC_API_KEYinstead of exiting 1. An empty-but-set value outranks keyless auth, so the unset is what makes federation reachable at all. It sits in the samerun:block as the scan, so it covers both the python process and theclaudeit spawns.claudecode/claude_api_client.py— constructAnthropic()with no key when none is present and let the SDK resolve environment credentials, instead of raisingValueError.claudecode/github_action_audit.py—validate_claude_available()accepts a static credential or federation env, with an error message naming both.Passing
claude-api-keybehaves exactly as before — this only adds a fallback path when it is absent.Two fork-local fixes on top of the upstream patch:
usage/_record_usageblock;logger.info("Claude API client initialized successfully")had ended up as the last statement of_record_usagein Report scan cost: Claude Code cost/usage envelope plus filter token usage #3, so it logged on every API response. Moved back into__init__.How the caller authenticates
The action deliberately does not mint the OIDC token itself. The calling workflow (
vuln-synthesis-agent/ai-security-scan) sets up federation the wayanthropics/claude-code-actiondoes: the GitHub OIDC token is written to a file withANTHROPIC_IDENTITY_TOKEN_FILEpointing at it, plus a minimal profile underANTHROPIC_CONFIG_DIR.The profile is not cosmetic. A GitHub assertion carries a
jtiand our federation issuer hascheck_jti: true, so one assertion buys exactly one token exchange. This action starts more than one Anthropic client per run — theclaudeCLI (up to 3 retry processes in_run_claude_code) plus the Python false-positive filter — and resolving federation through a profile turns on the SDK's on-disk credentials cache so they share a single exchanged token. Bare env-var federation would 401 withjti_reusedon the second consumer.Verified
py_compileon both changed modules,action.ymlparses as valid YAML.Anthropic(api_key=...)construction).200,/v1/messages200,/v1/messages/batches200— with noanthropic-beta: oauth-2025-04-20header, confirming it is not required for a federated token.🤖 Generated with Claude Code