Claude Platform Docs
AdminIdentity providers

Use WIF with GitHub Actions

Authenticate GitHub Actions workflows to the Claude API with short-lived identity tokens instead of long-lived API keys.

Every GitHub Actions workflow run can request a signed identity token from GitHub’s hosted issuer at https://token.actions.githubusercontent.com. With Workload Identity Federation (WIF), your workflow exchanges that token for a short-lived Anthropic access token. Your CI jobs can then call the Claude API without an ANTHROPIC_API_KEY secret stored in your repository.

The token’s sub claim encodes the repository and trigger context. For a push to a branch it has the form repo:<owner>/<repo>:ref:refs/heads/<branch>. Pull-request runs use repo:<owner>/<repo>:pull_request, and environment-gated deployments use repo:<owner>/<repo>:environment:<name>. Your federation rule matches against this claim (and others, such as repository_owner and ref) to determine which workflow runs can authenticate.

Prerequisites

  • Familiarity with WIF concepts: service accounts, federation issuers, and federation rules.
  • A GitHub repository where you can edit workflow files and grant the id-token: write permission.
  • Permission to create service accounts, federation issuers, and federation rules in the Claude Console for your Anthropic organization.
  • Your Anthropic organization ID. You can find it on the Organization page (platform.claude.com/settings/organization) in the Claude Console.

Configure your workflow

GitHub only issues an identity token to jobs that explicitly request it. Add the id-token: write permission at the workflow or job level:

permissions:
  id-token: write
  contents: read

Inside the job, the runner exposes two environment variables: ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN. Call the request URL with the request token as a bearer credential and your chosen audience as a query parameter. Then write the returned JSON Web Token (JWT) to a file:

- name: Fetch GitHub OIDC token
  run: |
    curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
      "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" \
      | jq -r .value > /tmp/gha-jwt

If you prefer JavaScript, actions/github-script exposes the same capability through core.getIDToken(audience):

- name: Fetch GitHub OIDC token
  uses: actions/github-script@v8
  with:
    script: |
      const fs = require('fs');
      const token = await core.getIDToken('https://api.anthropic.com');
      fs.writeFileSync('/tmp/gha-jwt', token);

The decoded token carries claims that describe the workflow run. Your federation rule matches against these:

{
  "iss": "https://token.actions.githubusercontent.com",
  "sub": "repo:your-org/your-repo:ref:refs/heads/main",
  "aud": "https://api.anthropic.com",
  "repository": "your-org/your-repo",
  "repository_owner": "your-org",
  "ref": "refs/heads/main",
  "sha": "abc123...",
  "workflow": "CI",
  "actor": "octocat",
  "event_name": "push"
}

See GitHub’s OIDC subject claim reference for the full list of sub formats.

Configure Anthropic

In the Claude Console, go to the Workload identity page (platform.claude.com/settings/workload-identity-federation), click Connect workload, and select the GitHub Actions tile. The wizard walks you through registering the issuer, creating a service account, and creating a federation rule.

The wizard creates these resources for you. Use the following values whether you enter them in the wizard or send them to the Admin API:

Federation issuer: GitHub publishes its OIDC discovery document and JWKS publicly, so use discovery mode. Anthropic refreshes the keys automatically when GitHub rotates them.

{
  "name": "github-actions",
  "issuer_url": "https://token.actions.githubusercontent.com",
  "jwks": { "type": "discovery" }
}

Federation rule: Match only the workflow runs you intend to trust. See Restrict which workflows can authenticate for how to scope these claims safely.

{
  "name": "gha-main",
  "issuer_id": "fdis_...",
  "match": {
    "subject_prefix": "repo:your-org/your-repo:ref:refs/heads/main",
    "audience": "https://api.anthropic.com",
    "claims": {
      "repository_owner": "your-org"
    }
  },
  "target": {
    "type": "service_account",
    "service_account_id": "svac_..."
  },
  "workspace_id": "wrkspc_...",
  "oauth_scope": "workspace:developer",
  "token_lifetime_seconds": 600
}

Be as specific as the workload allows. Loosen subject_prefix to repo:your-org/your-repo:* (paired with a claims.ref constraint) only if the rule must match multiple event types from the same repository. The trailing segment of sub varies among ref:..., environment:..., and pull_request events.

Acquire and use a token

Set the federation environment variables on the job, and write GitHub’s OIDC token to the file that ANTHROPIC_IDENTITY_TOKEN_FILE names:

name: Call Claude
on: push

permissions:
  id-token: write
  contents: read

jobs:
  call-claude:
    runs-on: ubuntu-latest
    env:
      ANTHROPIC_FEDERATION_RULE_ID: fdrl_...
      ANTHROPIC_ORGANIZATION_ID: 00000000-0000-0000-0000-000000000000
      ANTHROPIC_SERVICE_ACCOUNT_ID: svac_...
      ANTHROPIC_WORKSPACE_ID: wrkspc_...  # required when the rule covers multiple workspaces
      ANTHROPIC_IDENTITY_TOKEN_FILE: /tmp/gha-jwt
    steps:
      - uses: actions/checkout@v5
      - name: Fetch GitHub OIDC token
        run: |
          curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
            "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" \
            | jq -r .value > "$ANTHROPIC_IDENTITY_TOKEN_FILE"
      - name: Run your script
        run: |
          pip install anthropic
          python your_script.py

Then call the Claude API from your script. An SDK client that you create without credentials reads ANTHROPIC_IDENTITY_TOKEN_FILE, exchanges the JWT on the first request, and refreshes the access token automatically before it expires.

import anthropic

# Reads ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
# ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, and ANTHROPIC_IDENTITY_TOKEN_FILE
# from the job environment.
client = anthropic.Anthropic()

message = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello, Claude"}],
)
print(next(block.text for block in message.content if block.type == "text"))

Each GitHub-issued identity token expires roughly 5 minutes after issuance. The token-request endpoint (ACTIONS_ID_TOKEN_REQUEST_URL) stays valid for the entire job, so you can fetch a fresh token at any point. The SDK exchanges the token on first use and caches the resulting Anthropic access token. On each refresh, the SDK rereads ANTHROPIC_IDENTITY_TOKEN_FILE. For jobs that run longer than the Anthropic token’s lifetime, rerun the fetch step periodically (or wrap it in a background loop) to keep the file current. Alternatively, except in Java, pass the SDK a token provider that calls ACTIONS_ID_TOKEN_REQUEST_URL directly instead of using the file path.

Verify the setup

A successful exchange returns an access_token starting with sk-ant-oat01- and an expires_in value in seconds. A denied exchange returns an opaque 401 authentication_error with the fixed message Authentication failed, whichever check failed. In most cases, the attempt’s entry on the authentication history page in the Claude Console shows the deny reason. Troubleshoot a failed exchange walks through the checks in order. The most common GitHub Actions-side cause is a sub claim format that doesn’t match, because its trailing segment varies among ref:..., environment:..., and pull_request events. The history entry then shows the reason match_subject_prefix.

Restrict which workflows can authenticate

Lock the rule’s match block to the narrowest scope that fits your use case:

  • Pin to a single repository: Use subject_prefix: "repo:your-org/your-repo:*" so other repositories in the organization do not match.
  • Pin to a protected branch: Add "ref": "refs/heads/main" (or your release branch) under claims so pull-request runs and feature branches do not match.
  • Pin the owner explicitly: Add "repository_owner": "your-org" under claims as a defense-in-depth check against sub parsing edge cases.
  • Pin to a deployment environment: For deploy jobs, match subject_prefix: "repo:your-org/your-repo:environment:production" and gate that environment with required reviewers in GitHub.

Next steps

Was this page helpful?