`apiKeyHelper` output that is a `claude setup-token` OAuth token is rejected with 401; the same token works via `CLAUDE_CODE_OAUTH_TOKEN`
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?
apiKeyHelperoutput that is an OAuth token (prefixsk-ant-oat01-) is sent the wayCLAUDE_CODE_OAUTH_TOKENis sent, instead of as an API key.- A sibling setting (e.g.
oauthTokenHelper) with the same contract asapiKeyHelper(shell command, stdout is the credential,CLAUDE_CODE_API_KEY_HELPER_TTL_MS, re-run on 401/403) whose output takes theCLAUDE_CODE_OAUTH_TOKENprecedence 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
claude setup-tokenand copy thesk-ant-oat01-…value (it is printed together with the interactive UI, so$(claude setup-token)captures 28 lines; take only the token).- 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
- 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
401 API key is invalid`, 11 helper invocations, exit 1.
Result:
- Control:
rm -rf /tmp/cc-oat; mkdir -p /tmp/cc-oat; echo '{}' > /tmp/cc-oat/settings.jsonthen
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-tokenwrites 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