`apiKeyHelper` output that is a `claude setup-token` OAuth token is rejected with 401; the same token works via `CLAUDE_CODE_OAUTH_TOKEN`

Status Open
Reported on v2.1.281
Maintainer reply None cached
Activity 0 comments · opened Sep 26, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When apiKeyHelper prints a long-lived OAuth token obtained from claude setup-token (sk-ant-oat01-…, single line, no trailing newline), every API request fails with 401 authentication_error: API key is invalid. Claude Code retries the helper 11 times with backoff (about 3 minutes) and then reports Failed to authenticate.

The identical token works on the first attempt through CLAUDE_CODE_OAUTH_TOKEN and also through ANTHROPIC_AUTH_TOKEN. So the token is valid; the helper path presents it as an API key (X-Api-Key), and the API rejects it. The debug log confirms the mode: [claudeai-mcp] Disabled: API-key auth precedence active and [Bootstrap] Skipped: no usable OAuth, WIF, or API key.

The docs (Settings reference, apiKeyHelper) say the helper output is sent as both X-Api-Key and Authorization: Bearer, and setup-token is documented as the way to obtain a long-lived credential for non-interactive use. A subscription user therefore has no supported way to supply their credential dynamically: the only hook (apiKeyHelper) does not accept the only long-lived subscription credential (setup-token), and the two working paths are static environment variables readable by every child process of claude.

What Should Happen?

  1. apiKeyHelper output that is an OAuth token (prefix sk-ant-oat01-) is sent the way CLAUDE_CODE_OAUTH_TOKEN is sent, instead of as an API key.
  2. A sibling setting (e.g. oauthTokenHelper) with the same contract as apiKeyHelper (shell command, stdout is the credential, CLAUDE_CODE_API_KEY_HELPER_TTL_MS, re-run on 401/403) whose output takes the CLAUDE_CODE_OAUTH_TOKEN precedence slot.

Motivation: with a dynamic hook, the subscription credential can live in a hardware-bound store (Secure Enclave key with biometryCurrentSet, Touch ID per use, no plaintext Keychain item) instead of the Claude Code-credentials item that any same-user process reads silently (security find-generic-password -s "Claude Code-credentials" -w; see #77697, #89985, #94491, #96385, #73582). Today that is only achievable for API-key users.

Error Messages/Logs

Output of `claude -p 'reply with exactly: ok'` with the helper configured:


Failed to authenticate. API Error: 401 API key is invalid.


`--debug-file` excerpt (timestamps trimmed):


[DEBUG] [claudeai-mcp] Disabled: API-key auth precedence active
[DEBUG] [Bootstrap] Skipped: no usable OAuth, WIF, or API key
[ERROR] API error (attempt 1/11): 401 401 {"type":"error","error":{"type":"authentication_error","message":"API key is invalid."},"request_id":null}
…
[ERROR] API error (attempt 11/11): 401 401 {"type":"error","error":{"type":"authentication_error","message":"API key is invalid."},"request_id":null}
[ERROR] Error in API request: 401 {"type":"error","error":{"type":"authentication_error","message":"API key is invalid."},"request_id":null}
[ERROR] API auth_error: API key is invalid.


Control runs with the same token, same fresh config dir, no helper:


CLAUDE_CODE_OAUTH_TOKEN=<token> claude -p 'reply with exactly: ok'   -> ok   (exit 0, first attempt)
ANTHROPIC_AUTH_TOKEN=<token>    claude -p 'reply with exactly: ok'   -> ok   (exit 0, first attempt)

Steps to Reproduce

  1. claude setup-token and copy the sk-ant-oat01-… value (it is printed together with the interactive UI, so $(claude setup-token) captures 28 lines; take only the token).
  2. Create a helper that prints exactly the token and nothing else:

``sh
printf '#!/bin/sh\nprintf %%s "%s"\n' "$TOKEN" > /tmp/oat-helper.sh && chmod +x /tmp/oat-helper.sh
``

  1. Fresh config dir with only the helper configured:

``sh
export CLAUDE_CONFIG_DIR=/tmp/cc-oat && mkdir -p $CLAUDE_CONFIG_DIR
echo '{ "apiKeyHelper": "/tmp/oat-helper.sh" }' > $CLAUDE_CONFIG_DIR/settings.json
claude -p 'reply with exactly: ok' --output-format text --debug-file /tmp/cc-oat.log < /dev/null
`
Result:
401 API key is invalid`, 11 helper invocations, exit 1.

  1. Control: rm -rf /tmp/cc-oat; mkdir -p /tmp/cc-oat; echo '{}' > /tmp/cc-oat/settings.json then

CLAUDE_CODE_OAUTH_TOKEN=$TOKEN claude -p 'reply with exactly: ok' --output-format text → ok.

Same result with CLAUDE_CODE_API_KEY_HELPER_TTL_MS at 1 and at 43200000.

Claude Model

None

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.281 (native install, ~/.local/share/claude/versions/2.1.281)

Platform

Claude Code

Operating System

macOS

Terminal/Shell

iTerm2

Additional Information

Two smaller things noticed while reproducing, filed here as context rather than separate bugs:

  • On a 401 the helper is re-run on every attempt, 11 times with backoff (1, 1, 3, 5, 9, 19, 39, 34, 35, 34 s). A helper that prompts the user on each call turns one bad token into eleven prompts. A single re-run, or a retry hint in the helper's environment, would let helpers behave.
  • claude setup-token writes the token and its interactive UI to the same stdout, so the documented CI usage $(claude setup-token) needs post-processing. A --print-style quiet mode would help.

Repro scripts (fake-key stage, real-token stage, both controls): https://github.com/shipibo-studio/TegataApp/tree/main/spikes/M0/r7-helper

View original on GitHub ↗