Skip to content

[BUG] CLAUDE_CONFIG_DIR does not isolate macOS Keychain credentials per config directory #70697

Description

@jesseclark

Preflight checklist


What's wrong?

CLAUDE_CONFIG_DIR correctly isolates config files (.claude.json, settings, history) into separate directories, enabling multi-account workflows. However, on macOS, all Claude Code processes — regardless of which CLAUDE_CONFIG_DIR they use — read from and write to the same Keychain entry: "Claude Code-credentials".

This means:

  1. Concurrent sessions overwrite each other's tokens. When two terminal sessions run with different CLAUDE_CONFIG_DIR values (pointing to different accounts), whichever session last performs an OAuth refresh writes its new token to the shared Keychain entry. The other session's in-memory token is now stale, and its next refresh will fail (see also [BUG] OAuth refresh returns 400 after early 401 before local expiresAt; concurrent sessions forced to /login #54443).

  2. Rate limits are not account-isolated. Both sessions authenticate as whichever account last wrote to the Keychain, so usage and rate limits are attributed to that account regardless of which CLAUDE_CONFIG_DIR is set.

  3. The env var workaround is destructive. Using CLAUDE_CODE_OAUTH_TOKEN to bypass Keychain sharing deletes the Keychain entry on process exit (CLAUDE_CODE_OAUTH_TOKEN silently deletes macOS Keychain credentials on exit #37512), breaking every other session — including VS Code extensions and other terminals.

The Linux/WSL2 equivalent (credential path not scoped to CLAUDE_CONFIG_DIR) was tracked in #47661. This is the macOS counterpart, where the credential store is the Keychain rather than a plaintext file.


Steps to reproduce

# ~/.zshrc
alias claude-work='CLAUDE_CONFIG_DIR=$HOME/.claude-work claude'
alias claude-home='CLAUDE_CONFIG_DIR=$HOME/.claude-home claude'
  1. Log in to account A in ~/.claude-work, log in to account B in ~/.claude-home.
  2. Open two terminals. Run claude-work in one, claude-home in the other.
  3. In the claude-work terminal, trigger an OAuth refresh (use Claude until the token expires or run a long session).
  4. Observe: the claude-home session now authenticates as account A (the work account), not account B.
  5. Confirm with: security find-generic-password -s "Claude Code-credentials" -w — there is only one entry, and it reflects whichever account last refreshed.

What should happen?

When CLAUDE_CONFIG_DIR is set, the Keychain service name should be scoped to that directory so that each config directory maintains an isolated credential entry. For example:

CLAUDE_CONFIG_DIR Keychain service name
(unset / default) Claude Code-credentials
~/.claude-work Claude Code-credentials (~/.claude-work)
~/.claude-home Claude Code-credentials (~/.claude-home)

This would give CLAUDE_CONFIG_DIR true isolation across all stored state — matching what it already does for configs and history.


Environment

  • Claude Code version: latest
  • Platform: macOS
  • OS version: macOS 26 (Tahoe) 26.4.1
  • Shell: zsh
  • Terminal: Terminal.app / iTerm2

Additional context

The community has built several account-management tools (CCM, claude-swap, CCSwitcher) specifically to work around this limitation, all of which manually save and restore the single shared Keychain entry. This confirms the demand for native isolation. A PR to CCM that adds CLAUDE_CONFIG_DIR awareness to credential lookup (upstream dr5hn/ccm#4) further illustrates the pattern.

Related issues:

Metadata

Metadata

Assignees

No one assigned

    Labels

    duplicateThis issue or pull request already exists

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions