Part of the AI Coding Harness Guide — environment design for coding agents in production.

Claude Code apiKeyHelper Not Re-run on 401 with Vertex: "Google Cloud credentials expired or invalid"#

Published by

In Vertex mode with CLAUDE_CODE_SKIP_VERTEX_AUTH=1, Claude Code v2.1.316 does not re-run apiKeyHelper when your gateway answers 401, and it reports the error as a Google Cloud credential problem. To limit the damage, keep CLAUDE_CODE_API_KEY_HELPER_TTL_MS well below your token lifetime and resend once after an idle gap. To fix it, use Anthropic API mode through the gateway.[1]

Why the expired token is not refreshed#

apiKeyHelper output is cached. Claude Code has three ways to replace a cached value, and in v2.1.316 two of them apply only when the provider is the Anthropic API (no CLAUDE_CODE_USE_VERTEX, CLAUDE_CODE_USE_BEDROCK or other cloud-provider flag), apiKeyHelper is the credential source, and ANTHROPIC_AUTH_TOKEN is unset:

  • Re-run on 401 or 403. The cache is cleared and the request is retried with fresh helper output. This is the case the settings reference describes for "a request to the Anthropic API, directly or through an LLM gateway".
  • Expired JWT before sending. When the cached value is a JWT whose exp is less than 30 seconds away, the helper runs and the request waits for it.
  • Cache lifetime. After CLAUDE_CODE_API_KEY_HELPER_TTL_MS (default 300,000 ms, 5 minutes), the next request still sends the cached value, and the helper runs in the background for the request after it. This one applies in every mode.

In Vertex mode only the third applies. Claude Code classifies any Vertex 401 as a Google Cloud credential error, shows "Google Cloud credentials expired or invalid", and does not retry. gcpAuthRefresh, the Vertex refresh hook, refreshes Google Cloud credentials, which a gateway setup with CLAUDE_CODE_SKIP_VERTEX_AUTH=1 does not use.

The report is issue #105408 (27 reactions, opened 2026-10-27): a gateway that takes a 60-minute Okta ID token from apiKeyHelper fails every request with the same expired token until the cache lifetime runs out. The reporter says the same helper recovered on 401 with CLAUDE_CODE_USE_BEDROCK=1 and CLAUDE_CODE_SKIP_BEDROCK_AUTH=1.

When a request fails#

Because the cache lifetime refresh serves the old value once, a request fails when both of these hold:

ConditionEffect
The cache lifetime has run out since the helper last ranThe request sends the cached value and starts a background refresh
The cached token has expired at the gatewayThe gateway answers 401, Claude Code shows the Google Cloud error and does not retry

The background refresh still runs, so the next message goes out with a fresh token. A cache lifetime equal to the token lifetime (for example 3600000 for a 60-minute token) makes the failure regular: the cached token expires before the cache does, and every request in between fails.

How to work around it#

1. Keep the cache lifetime short. Leave CLAUDE_CODE_API_KEY_HELPER_TTL_MS unset (5 minutes) or set it well below the token lifetime. Each request after the lifetime then starts a refresh while the cached token is still valid:

{
  "apiKeyHelper": "~/bin/gateway-token.sh",
  "env": {
    "CLAUDE_CODE_USE_VERTEX": "1",
    "CLAUDE_CODE_SKIP_VERTEX_AUTH": "1",
    "CLAUDE_CODE_API_KEY_HELPER_TTL_MS": "300000"
  }
}

With this, only the first request after an idle gap longer than the token lifetime fails. Send it again.

2. Make the helper cheap to call often. A short cache lifetime runs the helper more often. If your identity provider limits token requests, have the script keep its own cached token and print it until a few minutes before its exp, then fetch a new one.

3. Use Anthropic API mode if the gateway supports it. If your gateway also accepts Anthropic Messages API requests, drop CLAUDE_CODE_USE_VERTEX and point Claude Code at it with ANTHROPIC_BASE_URL. In that mode a 401 or 403 re-runs the helper and retries, and an expired JWT is refreshed before sending. Leave ANTHROPIC_AUTH_TOKEN unset: when it is set, the helper is not re-run on 401.

{
  "apiKeyHelper": "~/bin/gateway-token.sh",
  "env": {
    "ANTHROPIC_BASE_URL": "https://llm-gateway.example.com"
  }
}

Timeline#

VersionPublishedChange
v0.2.742025-04-18apiKeyHelper output is refreshed, with a 5-minute cache lifetime.
v0.2.1172025-05-17CLAUDE_CODE_API_KEY_HELPER_TTL_MS sets the cache lifetime.
v2.1.2462026-08-25An expired cached JWT is refreshed before sending, and 401/403 errors retry quietly.[2]
v2.1.3162026-10-31Both v2.1.246 behaviors still apply only in Anthropic API mode. Vertex 401 is still reported as a Google Cloud credential error.

Should you change your setup?#

If your gateway speaks only the Vertex API, use workaround 1 and expect one failed request after a long idle gap until issue #105408 is fixed. If it also speaks the Anthropic API, workaround 3 removes the failure, at the cost of moving the gateway route to a different API format.


FAQ#

Q: Why does Claude Code say "Google Cloud credentials expired or invalid" when I use a gateway token?

A: In Vertex mode, Claude Code treats every 401 as a Google Cloud credential error, even with CLAUDE_CODE_SKIP_VERTEX_AUTH=1, where the rejected credential is your apiKeyHelper output.

Q: Does apiKeyHelper re-run on 401 with Vertex?

A: No, not in v2.1.316. The re-run on 401 or 403 and the expired-JWT refresh apply only in Anthropic API mode, with apiKeyHelper as the credential source and ANTHROPIC_AUTH_TOKEN unset.

Q: What does CLAUDE_CODE_API_KEY_HELPER_TTL_MS do?

A: It sets how long, in milliseconds, cached helper output is used before the helper runs again. The default is 300,000 (5 minutes). After it runs out, the next request still sends the cached value, and the refresh finishes in the background.

Q: What value should I use for CLAUDE_CODE_API_KEY_HELPER_TTL_MS with a 60-minute token?

A: Well below 60 minutes. The default of 5 minutes works. A value equal to the token lifetime makes requests fail with the expired token until the cache runs out.

Q: Does gcpAuthRefresh fix it?

A: No. gcpAuthRefresh refreshes Google Cloud credentials, and a setup with CLAUDE_CODE_SKIP_VERTEX_AUTH=1 does not use them.


Sources: [1] Claude Code changelog | [2] Claude Code changelog, v2.1.246 | Issue #105408

Related: CLAUDE_CODE_API_KEY_HELPER_TTL_MS in the env-var glossary | CLAUDE_CODE_SKIP_VERTEX_AUTH in the env-var glossary | Claude Code v2.1.247 analysis | CA certificates guide