Skip to content

WIF federation fails with 401 (jti_reused) when plugins are configured — each spawned claude process re-exchanges the single-use OIDC token #1406

Description

@KeisukeYamashita

Describe the bug

When workload identity federation (WIF) is used together with the plugins / plugin_marketplaces inputs, the action reliably fails with:

Claude Code returned an error result: API Error: Token exchange failed with status 401
(request-id req_...): {"error":{"type":"authentication_error","message":"Authentication failed"}}
Ensure your federation rule matches your identity token.

The run dies approximately 3 minutes after start with num_turns: 1 and total_cost_usd: 0 — no API call ever succeeds. The Claude Console authentication history fills up with platform_federated_authentication failure events with reason jti_reused.

Removing the plugins input, with no other workflow changes, makes the same workflow succeed.

Root cause

This was traced with an eBPF process/network sensor on the runner and reproduced locally against a mock that enforces single-use jti.

  1. A GitHub OIDC token is single-use at the Anthropic token-exchange endpoint. The same jti cannot be exchanged twice.

  2. With plugins configured, the action spawns several short-lived claude processes per job:

    • claude plugin marketplace add
    • one claude plugin install per plugin
    • the main query, via the Agent SDK
  3. Each process resolves federation from the bare env vars:

    • ANTHROPIC_FEDERATION_RULE_ID
    • other related federation env vars
    • ANTHROPIC_IDENTITY_TOKEN_FILE

    Each process then performs its own token exchange using the same identity-token file.

    The first process succeeds. Every subsequent process re-presents the now-consumed jti and receives 401 jti_reused.

  4. By the time the main query runs, the token has already been burned. It retries the exchange with exponential backoff for approximately 3 minutes. The same file still contains the same jti, because the action's background refresh runs every 4 minutes, and the job eventually fails.

eBPF process trace from a failing run — five separate claude processes each contact api.anthropic.com with the same identity-token file:

16:06:36  pid 2225  claude (native binary, install-time handshake)
16:06:45  pid 2259  claude plugin marketplace add
16:06:46  pid 2306  claude plugin install differential-review@trailofbits
16:06:47  pid 2315  claude plugin install fp-check@trailofbits
16:06:48  pid 2324  claude (main query, agent-sdk)        ← fails, token already consumed

The federation rule itself is fine. Exchanging the same workflow's OIDC token once via a raw curl to POST /v1/oauth/token returns 200, and a second exchange of the same JWT returns the exact 401 above.

To reproduce

  1. Configure WIF using a federation rule for a GitHub Actions repo, following the docs.
  2. Run the workflow below on a GitHub-hosted runner using ubuntu-24.04.
  3. The run fails after approximately 3 minutes with the token-exchange 401.
  4. The Console shows one successful exchange followed by repeated jti_reused failures for the same jti.
  5. Remove the plugins / plugin_marketplaces inputs.
  6. The run succeeds.

This was reproduced on:

  • v1.0.139, which bundles Claude Code 2.1.167
  • v1.0.144, which bundles Claude Code 2.1.173

This does not appear to be fixed by version bumps.

In our org, this happened on 100% of runs with plugins — 10+ runs across two repos and three workflows — and 0% of runs without plugins.

Expected behavior

Plugins and WIF should work together.

The exchanged access token should be shared across the claude processes spawned by the action, so the single-use identity token is only exchanged once per token lifetime.

Workflow YAML file

Minimal reproduction:

name: wif-plugins-repro

on: workflow_dispatch

permissions:
  id-token: write
  contents: read

jobs:
  repro:
    runs-on: ubuntu-24.04

    steps:
      - uses: actions/checkout@v4

      - uses: anthropics/claude-code-action/base-action@v1.0.144
        with:
          prompt: "Say hi in one word and stop. Do not use any tools."
          anthropic_federation_rule_id: "fdrl_***"
          anthropic_organization_id: "***"
          anthropic_service_account_id: "svac_***"
          anthropic_workspace_id: "wrkspc_***"
          claude_args: '--model claude-opus-4-6 --allowedTools "Read"'
          plugin_marketplaces: |
            https://github.com/trailofbits/skills.git
          plugins: |
            differential-review@trailofbits
            fp-check@trailofbits

API Provider

  • Anthropic First-Party API — default
  • AWS Bedrock
  • GCP Vertex

Additional context

Sample failing request IDs, in UTC, in case server-side logs help:

  • req_011CbwgekzRwSukwKoVFBMnm — 2026-06-11 15:09
  • req_011CbwiRpxvNGfJSs13mMVnT — 2026-06-11 around 16:09

Happy to share the federation rule / org IDs privately.

The jti_reused events are misleading during diagnosis. The first event per jti is a success, or the real error, and everything after that is retry noise. We initially chased the federation rule configuration because of this.

Proposed fix

Happy to open a PR.

The Anthropic SDK only enables its on-disk credentials cache:

<config_dir>/credentials/<profile>.json

This cache is shared across processes, but it is only enabled when federation is loaded from a profile config file — not from bare env vars.

If setupWorkloadIdentity() additionally writes a profile pointing at the identity-token file and selects it via:

  • ANTHROPIC_CONFIG_DIR
  • ANTHROPIC_PROFILE

then the first process exchanges once, and every other process reuses the cached access token.

The env vars can remain as a fallback for older CLIs.

Implemented and verified here:

https://github.com/KeisukeYamashita/claude-code-action/compare/main...fix/wif-shared-credentials-cache

With that change, the exact configuration above, which previously failed 100% of the time, succeeds:

  • is_error: false
  • main query completes in approximately 6 seconds
  • the eBPF trace shows the same five processes, but only one token exchange
  • existing test suite passes: 702/702
  • new unit tests were added for the profile file

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingp2Non-showstopper bug or popular feature requestprovider:1pAnthropic First-Party API

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions