Skip to content

claude login: read the shared Keychain credential when the CLI doesn't namespace by config dir - #20

Open
rbutera wants to merge 1 commit into
RubricLab:mainfrom
rbutera:fix/claude-login-shared-keychain
Open

claude login: read the shared Keychain credential when the CLI doesn't namespace by config dir#20
rbutera wants to merge 1 commit into
RubricLab:mainfrom
rbutera:fix/claude-login-shared-keychain

Conversation

@rbutera

@rbutera rbutera commented Aug 6, 2026

Copy link
Copy Markdown

Problem

tokenmaxx login claude reports success but never stores an account, so the proxy later answers every request with 503 no active anthropic account.

registerClaudeAccount runs an isolated claude auth login --claudeai under a temporary CLAUDE_CONFIG_DIR, then importCliCredential reads the credential back from the macOS Keychain under a service name namespaced by a hash of that config dir (Claude Code-credentials-<hash8>). Current Claude Code (checked on v2.1.220 and v2.1.223) stores its OAuth credential under the plain service Claude Code-credentials regardless of CLAUDE_CONFIG_DIR, and writes no .credentials.json on macOS. Both read branches miss, importCliCredential throws CREDENTIAL_MISSING, and no account row is written. The inner login has already printed "Login successful", so from the outside it looks like it worked and the account simply never appears in the list.

Fix

Read whichever store the CLI actually wrote. Try the namespaced service first, so any build that does namespace keeps working, then fall back to the shared Claude Code-credentials service (keeping the same -a <user> filter), then the on-disk file.

removeClaudeProfile still deletes only the namespaced service, so cleanup never touches the user's own shared Claude Code login.

Evidence

On v2.1.223, security dump-keychain shows only the plain Claude Code-credentials service with no namespaced variant, and that entry carries the claudeAiOauth object the schema expects. The -a <user> filter matters: without it the same service name resolves to a different mcpOAuth-only entry, so the read keeps the user filter.

Tests

Added a regression test: the namespaced lookup misses, the shared service returns the credential, and registerClaudeAccount persists it. The existing namespaced-path test still passes. bun test, typecheck, and biome are all clean.

The version bump and changelog line came from the repo's own prepare commit hook; drop or reformat them if you'd rather handle that at release.

@DexterStorey

Copy link
Copy Markdown
Contributor

Hey @rbutera, thanks for the PR! I tried to confirm the premise and got the opposite result, so let me share the test.

The PR's claim is that the CLI ignores CLAUDE_CONFIG_DIR and always uses the shared Claude Code-credentials keychain entry -- which would mean tokenmaxx's namespaced lookup can never find anything. To check that, I wrote a fake credential into the namespaced entry for a temp config dir (the exact entry tokenmaxx expects), then ran claude auth status with CLAUDE_CONFIG_DIR pointed at that dir:

  • With the fake credential planted: auth status reported logged in, with my fake data. So the CLI does read the namespaced entry.
  • With nothing planted: it reported logged out — even though my real login sat in the shared entry the whole time. So it doesn't fall back to shared either.

Same result on 2.1.235 and 2.1.221 (inside the version range you tested). That's the opposite of what the PR assumes, so something else must be going on in your setup, or I messed up, or misunderstood something!

Separate from the repro: reading the shared entry as a fallback is risky even when it works -- that entry is your main Claude Code login, so a miss on the namespaced read would make tokenmaxx silently import the wrong account and then compete with Claude Code over the same OAuth session.

Can you share claude --version, how it's installed (npm vs native), and whether your shell sets CLAUDE_CONFIG_DIR anywhere? Happy to help fix, or update my understanding!

@rbutera

rbutera commented Aug 19, 2026

Copy link
Copy Markdown
Author

Hey @DexterStorey, thanks for digging in and sharing the exact repro, that's genuinely helpful. I think we're both right and looking at two different halves of the flow.

Your test plants a credential in the namespaced entry and checks claude auth status, which proves the CLI READS namespaced first (and won't fall back to shared on read). I see the same. But my PR's evidence is about the WRITE side: where a fresh claude auth login --claudeai actually stores the credential when it runs under a temp CLAUDE_CONFIG_DIR. On my setup, after the isolated login, security dump-keychain showed the credential in the plain Claude Code-credentials service, with no namespaced entry ever created. So login wrote to shared while the read looks namespaced-first, and tokenmaxx's namespaced-only read misses what login actually wrote. That also explains the disagreement: you planted the entry the code expects, so the write path never came into your repro.

The variable I'd bet on is install method. My setup:

  • claude 2.1.235
  • native install (not npm): ~/.local/bin/claude resolves to ~/.local/share/claude/versions/2.1.235, a Mach-O arm64 binary
  • CLAUDE_CONFIG_DIR is not set anywhere (shell or env)

Decisive test if you can: on a NATIVE install, run an actual claude auth login --claudeai with CLAUDE_CONFIG_DIR pointed at a fresh temp dir, then security dump-keychain | grep "Claude Code". My bet is it lands in the shared service, not the namespaced one. If native and npm builds differ on where login writes, that's the whole discrepancy.

On the safety point, you're right and I don't want to import the wrong account or fight Claude Code over the OAuth session. Two mitigations already in the PR: the fallback read keeps the same -a <user> filter, so it only imports the account matching the user who just logged in, and removeClaudeProfile still deletes only the namespaced service, so cleanup never touches your real shared login. Happy to gate the shared fallback more tightly (only when the namespaced read misses AND the shared entry's account matches the expected user), or to reframe this as a native-build write-location bug to raise upstream and keep the fallback as the pragmatic bridge until then. Whatever you prefer.

@pedroapfilho

Copy link
Copy Markdown
Contributor

Yeah, since yesterday my tokenmaxx setup is broken, and is asking me to login both on codex and on claude

@DexterStorey

Copy link
Copy Markdown
Contributor

Hey @pedroapfilho this is a one-time migration cost we should have surfaced.

The update removed the dummy ANTHROPIC_AUTH_TOKEN from claude's settings (it was breaking claude.ai features) and switched the codex provider to native ChatGPT auth.

Both changes mean the CLIs now want their own login once: run codex login, and in claude run /login.

Your tokenmaxx accounts and routing should still work.

Also make sure you're on 0.0.65 and run tokenmaxx install once. codex 0.147 started rewriting config.toml in a way that could corrupt the routing block, and 0.0.65 repairs it. If anything still asks for login after that, send me your tokenmaxx doctor output.

@DexterStorey

DexterStorey commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Hey @rbutera!

I ran your decisive test on a setup matching yours exactly (native ~/.local/share/claude/versions/2.1.235, arm64, no CLAUDE_CONFIG_DIR set): the isolated login created a fresh namespaced entry, hash matching what tokenmaxx computes, shared entry untouched. So we get opposite results on identical versions — something else is different on your machine, and I'd like to find it.

One theory that fits your evidence: if the CLI on your machine stores the credential in the Data Protection keychain, the security CLI can't see it at all — which would explain both your empty dump and tokenmaxx's miss (and would mean the shared entry you imported might be a stale one, not the fresh login).

Could you run this and paste the output?

sw_vers; type -a claude; echo "env: ${CLAUDE_CONFIG_DIR:-unset}"
T=$(mktemp -d /tmp/cc-login-probe-XXXX)
H=$(printf '%s' "$T" | shasum -a 256 | cut -c1-8)
echo "expecting service: Claude Code-credentials-$H"
CLAUDE_CONFIG_DIR="$T" claude auth login --claudeai
CLAUDE_CONFIG_DIR="$T" claude auth status
security find-generic-password -a "$USER" -s "Claude Code-credentials-$H" -w >/dev/null 2>&1; echo "namespaced visible to security CLI: $? (0 = yes)"
security dump-keychain 2>/dev/null | grep '"svce"<blob>="Claude Code-credentials'

[before pasting, make sure there's nothing sensitive in the result. There shouldn't be.]

Then CLAUDE_CONFIG_DIR="$T" claude auth logout to clean up the test session.

The key line is the second-from-last: if auth status says logged in but that prints non-zero, the credential lives somewhere security can't reach, that's a real bug, and I'd fix tokenmaxx's read rather than add the shared fallback. If the namespaced entry shows up fine, we hunt for a wrapper or env reset on your machine instead.

Thank you so much for this back and forth, hopeful we can get to the bottom of this! 🔥

@rbutera

rbutera commented Aug 20, 2026

Copy link
Copy Markdown
Author

Hey @rbutera!

I ran your decisive test on a setup matching yours exactly (native ~/.local/share/claude/versions/2.1.235, arm64, no CLAUDE_CONFIG_DIR set): the isolated login created a fresh namespaced entry, hash matching what tokenmaxx computes, shared entry untouched. So we get opposite results on identical versions — something else is different on your machine, and I'd like to find it.

One theory that fits your evidence: if the CLI on your machine stores the credential in the Data Protection keychain, the security CLI can't see it at all — which would explain both your empty dump and tokenmaxx's miss (and would mean the shared entry you imported might be a stale one, not the fresh login).

Could you run this and paste the output?

sw_vers; type -a claude; echo "env: ${CLAUDE_CONFIG_DIR:-unset}"
T=$(mktemp -d /tmp/cc-login-probe-XXXX)
H=$(printf '%s' "$T" | shasum -a 256 | cut -c1-8)
echo "expecting service: Claude Code-credentials-$H"
CLAUDE_CONFIG_DIR="$T" claude auth login --claudeai
CLAUDE_CONFIG_DIR="$T" claude auth status
security find-generic-password -a "$USER" -s "Claude Code-credentials-$H" -w >/dev/null 2>&1; echo "namespaced visible to security CLI: $? (0 = yes)"
security dump-keychain 2>/dev/null | grep '"svce"<blob>="Claude Code-credentials'

[before pasting, make sure there's nothing sensitive in the result. There shouldn't be.]

Then CLAUDE_CONFIG_DIR="$T" claude auth logout to clean up the test session.

The key line is the second-from-last: if auth status says logged in but that prints non-zero, the credential lives somewhere security can't reach, that's a real bug, and I'd fix tokenmaxx's read rather than add the shared fallback. If the namespaced entry shows up fine, we hunt for a wrapper or env reset on your machine instead.

Thank you so much for this back and forth, hopeful we can get to the bottom of this! 🔥

Here is my output:

rai in  nimbus in ~ on ☁️
> sw_vers; type -a claude; echo "env: ${CLAUDE_CONFIG_DIR:-unset}"
T=$(mktemp -d /tmp/cc-login-probe-XXXX)
H=$(printf '%s' "$T" | shasum -a 256 | cut -c1-8)
echo "expecting service: Claude Code-credentials-$H"
CLAUDE_CONFIG_DIR="$T" claude auth login --claudeai
CLAUDE_CONFIG_DIR="$T" claude auth status
security find-generic-password -a "$USER" -s "Claude Code-credentials-$H" -w >/dev/null 2>&1; echo "namespaced visible to security CLI: $? (0 = yes)"
security dump-keychain 2>/dev/null | grep '"svce"<blob>="Claude Code-credentials'
ProductName:		macOS
ProductVersion:		26.4
BuildVersion:		25E246
claude is a shell function from /Users/rai/.aliases
claude is /Users/rai/.local/bin/claude
env: unset
expecting service: Claude Code-credentials-a2d485a8
Opening browser to sign in…
<!-- REDACTED -->

{
  "loggedIn": true,
  "authMethod": "claude.ai",
  "apiProvider": "firstParty",
  "email": "rai@rbutera.com",
  "orgId": "45f7df46-2a8c-4f1b-a131-c321063d6dc9",
  "orgName": "rai@rbutera.com's Organization",
  "subscriptionType": "max"
}

namespaced visible to security CLI: 44 (0 = yes)
    "svce"<blob>="Claude Code-credentials"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants