Manage multiple Claude auth profiles for the Claude Code CLI and Python Agent SDK usage.
Project description
claude-select
claude-select is a local SDK and CLI design for managing multiple Claude authentication profiles across:
- the global Claude Code CLI login state
- Python programs using the Claude Agent SDK
This repository now contains a working first implementation of the design described below:
- file-backed profile storage
- CLI profile capture, switch, sync, inspect, remove, and default SDK selection
- Python
build_sdk_env()support for directClaudeAgentOptions(env=...)usage - OAuth refresh handling for stored profiles
The README still documents the intended architecture so the implementation can evolve without losing the original design constraints.
Status
Current implementation status:
ProfileManagerand top-levelbuild_sdk_env()are implemented- CLI commands are implemented for local single-user usage
- OAuth refresh is implemented for stored profiles
- unit tests are in place
- lint, type-check, build, and CI configuration are included
Goals
- Let a user capture multiple Claude accounts/profiles on one machine.
- Let the global Claude Code CLI switch between stored profiles.
- Let Python programs select a profile explicitly for each Claude Agent SDK call.
- Share one profile store between CLI switching and Python SDK usage.
- Avoid coupling Python SDK requests to the current global CLI account.
Non-goals
- Replacing Claude's official login flow.
- Building a hosted multi-user auth product.
- Relying on global process-wide environment mutation for Python SDK calls.
- Treating Agent SDK auth and CLI live auth as the same runtime state.
Core Model
The design separates three concepts:
profilesStored account/auth profiles shared by CLI and Python SDK usage.current_cli_profileThe profile currently written into Claude Code's live login state.default_sdk_profileThe fallback profile used by Python helpers when the caller does not explicitly choose one.
This separation is intentional:
- CLI switching is global and mutates Claude's live state.
- Agent SDK switching is per-call and should be isolated through
env.
Authentication Model
CLI
For the Claude Code CLI, switching works by reading and writing Claude's live login state:
~/.claude.jsonor~/.claude/.config.json~/.claude/.credentials.json- platform-specific secure storage where applicable
CLAUDE_CONFIG_DIRwhen set
The SDK captures the current live state into a reusable profile, then later writes a chosen profile back into the live Claude files when the user switches.
Python Agent SDK
For Python, the preferred model is explicit per-call environment injection:
env = manager.build_sdk_env("work")
options = ClaudeAgentOptions(env=env)
This avoids:
- mutating global
os.environ - leaking one profile into another concurrent request
- forcing Python usage to follow whatever the CLI currently uses
Shared Store Design
The SDK owns a profile store separate from Claude's live runtime files.
Recommended structure:
~/.config/claude-select/
state.json
secrets/
work.json
personal.json
state.json
Holds non-sensitive metadata and pointers:
{
"version": 1,
"current_cli_profile": "personal",
"default_sdk_profile": "work",
"profiles": {
"work": {
"id": "work",
"kind": "oauth",
"label": "work",
"email": "user@example.com",
"organization_id": "org_xxx",
"organization_name": "Example Org",
"auth_state": "ok",
"expires_at": 1760000000000,
"secret_ref": "work",
"updated_at": "2026-04-19T00:00:00Z"
}
}
}
secrets/<profile>.json
Holds sensitive auth material:
{
"oauthAccount": {
"emailAddress": "user@example.com"
},
"credentials": {
"claudeAiOauth": {
"accessToken": "...",
"refreshToken": "...",
"expiresAt": 1760000000000,
"scopes": ["user:profile"]
}
}
}
Future versions may replace file-based secret storage with platform keychains while keeping the same secret_ref abstraction.
OAuth Expiry Strategy
When using OAuth-backed profiles:
- Access token near expiry: refresh automatically if
refreshTokenexists. - Refresh succeeds: update the profile store before returning.
- Refresh fails: mark the profile as
reauth_required.
For CLI switching:
- switching should still be allowed
- the tool should clearly report that the selected profile now requires
claude/login - after re-login, the user runs
captureorsyncto update the stored profile
For Python SDK usage:
build_sdk_env(profile)should attempt refresh first- if refresh fails, raise a clear exception such as
ProfileReauthRequired
CLI Design
Planned commands:
claude-select capture <profile>
claude-select sync [<profile>]
claude-select list
claude-select current
claude-select use <profile>
claude-select remove <profile>
claude-select set-default-sdk <profile>
Command behavior
capture <profile>Reads Claude's current live login state and stores it as a named profile.sync [<profile>]Updates an existing stored profile from the current live login state.listShows all stored profiles and auth state.currentShows the current CLI profile and default SDK profile.use <profile>Switches Claude's global live login state to the chosen profile.remove <profile>Removes a stored profile from the SDK store.set-default-sdk <profile>Updatesdefault_sdk_profile.
Python SDK Design
Planned primary interface:
from claude_select import ProfileManager
manager = ProfileManager()
env = manager.build_sdk_env("work")
Planned convenience function:
from claude_select import build_sdk_env
env = build_sdk_env("work")
Intended ProfileManager surface
class ProfileManager:
def list_profiles(self) -> list[str]: ...
def capture_cli_profile(self, name: str) -> None: ...
def sync_cli_profile(self, name: str | None = None) -> None: ...
def switch_cli(self, name: str) -> None: ...
def set_default_sdk_profile(self, name: str) -> None: ...
def get_default_sdk_profile(self) -> str | None: ...
def inspect_profile(self, name: str) -> dict: ...
def build_sdk_env(self, name: str | None = None) -> dict[str, str]: ...
Agent SDK Environment Injection
The direct usage style for Python is the intended default:
from claude_select import ProfileManager
from claude_code_sdk import ClaudeAgentOptions, query
manager = ProfileManager()
env = manager.build_sdk_env("work")
options = ClaudeAgentOptions(env=env)
async for message in query(
prompt="Analyze this repository",
options=options,
):
print(message)
Why this is the default
- explicit per-call behavior
- works with async and concurrent tasks
- no hidden global mutation
- lets one process use multiple profiles safely
Environment Variables by Profile Type
build_sdk_env() will return a clean environment map for exactly one auth mode.
OAuth profile
Expected output:
{
"CLAUDE_CODE_OAUTH_TOKEN": "...",
"CLAUDE_CODE_OAUTH_REFRESH_TOKEN": "..."
}
API key profile
Expected output:
{
"ANTHROPIC_API_KEY": "..."
}
Important rule
Conflicting auth env vars should be removed from the returned environment. For example, an OAuth profile should not leave these active:
ANTHROPIC_API_KEYANTHROPIC_AUTH_TOKENCLAUDE_CODE_USE_BEDROCKCLAUDE_CODE_USE_VERTEXCLAUDE_CODE_USE_FOUNDRY
How Users Get Started
Recommended first-run flow:
- Log in with Claude's official CLI flow.
claude
Then complete /login.
- Capture the current account into a named profile.
claude-select capture work
- Log in with another account if needed, then capture again.
claude-select capture personal
- Switch the global CLI account when needed.
claude-select use personal
- Use a chosen profile from Python.
from claude_select import ProfileManager
from claude_code_sdk import ClaudeAgentOptions
manager = ProfileManager()
env = manager.build_sdk_env("work")
options = ClaudeAgentOptions(env=env)
Development
Set up a local environment:
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -e ".[dev]"
Run the full local quality suite:
ruff check .
ruff format --check .
mypy
python3 -m pytest
python3 -m build
python3 -m twine check dist/*
You can also run the CLI as:
python3 -m claude_select --help
Runtime Relationship
The intended data flow is:
- CLI capture: Claude live state -> SDK profile store
- CLI switch: SDK profile store -> Claude live state
- Python SDK usage: SDK profile store ->
env
The Python SDK should not depend on the current global CLI account unless the caller explicitly chooses the same profile.
Safety and Concurrency
Implementation should include:
- file locking during capture, sync, switch, and refresh
- atomic writes for state and secret files
- backups before mutating Claude live files
- detection of running Claude CLI / IDE instances
- clear messaging when restart or re-login is required
Current status:
- file locking is implemented for the SDK profile store
- atomic file writes are implemented for profile and live-state file writes
- live-state backups are implemented before Claude auth mutation
- full Claude process detection is not implemented yet
Status Values
Each profile should expose a simple auth status:
okexpiring_soonrefreshablereauth_requiredinvalid
These statuses should be visible in list and inspect_profile.
Scope of First Implementation
The first implementation should prioritize:
- file-based profile store
- OAuth profile capture from Claude CLI live state
- CLI switching between stored OAuth profiles
build_sdk_env(profile)for Python Agent SDK usage- token refresh for OAuth-backed profiles
The first implementation should not prioritize:
- hosted sync
- multi-user remote storage
- GUI
- deep plugin integrations
Current Limitations
- The primary supported profile type today is OAuth captured from Claude CLI live state.
- macOS keychain reading and writing is implemented, but broader secure-store coverage is still incomplete.
- Full pre-switch detection of running Claude sessions and IDE integrations is not implemented yet.
- This project is designed for local single-user machines, not shared multi-user hosts.
Release Checklist
Before publishing a release:
- Run the full local quality suite.
- Verify
claude-select capture,claude-select use, andbuild_sdk_env()against a real local Claude setup. - Update
CHANGELOG.md. - Create a version tag and publish artifacts built from CI-verified sources.
References
This design is informed by the following projects:
Current State
The repository contains a tested first implementation aimed at local single-user usage. Future work can harden:
- platform keychain integration beyond the current file-backed path and macOS keychain support
- richer process detection before CLI switching
- broader profile kinds beyond OAuth-backed profiles
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file claude_select-0.1.0.tar.gz.
File metadata
- Download URL: claude_select-0.1.0.tar.gz
- Upload date:
- Size: 23.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a087065bb4cf56008f5aeb77092cf169ad5f6dec97c1eac6497cd1410a9d8760
|
|
| MD5 |
763a6a3595a280634df833f72321012b
|
|
| BLAKE2b-256 |
2e456faa332221a3bb39d7b8c0d6967df0376bda1a33a12ac43da71b3cf590ec
|
Provenance
The following attestation bundles were made for claude_select-0.1.0.tar.gz:
Publisher:
publish.yml on Nomia/claude-select
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claude_select-0.1.0.tar.gz -
Subject digest:
a087065bb4cf56008f5aeb77092cf169ad5f6dec97c1eac6497cd1410a9d8760 - Sigstore transparency entry: 1340483308
- Sigstore integration time:
-
Permalink:
Nomia/claude-select@f7e912b6fb2980301f340f47a08da77f05344174 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Nomia
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f7e912b6fb2980301f340f47a08da77f05344174 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file claude_select-0.1.0-py3-none-any.whl.
File metadata
- Download URL: claude_select-0.1.0-py3-none-any.whl
- Upload date:
- Size: 19.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
80222607d223ebf9a016dd7d380c9f786ab1bf9f00f894702cc18180e600aa6c
|
|
| MD5 |
573af11044d46e333439e8817d927123
|
|
| BLAKE2b-256 |
fdb25aaedb17d6f822936922e9bfd9edb1aa595ea79ec2ac72cdf04f57a8c942
|
Provenance
The following attestation bundles were made for claude_select-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on Nomia/claude-select
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claude_select-0.1.0-py3-none-any.whl -
Subject digest:
80222607d223ebf9a016dd7d380c9f786ab1bf9f00f894702cc18180e600aa6c - Sigstore transparency entry: 1340483309
- Sigstore integration time:
-
Permalink:
Nomia/claude-select@f7e912b6fb2980301f340f47a08da77f05344174 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Nomia
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f7e912b6fb2980301f340f47a08da77f05344174 -
Trigger Event:
workflow_dispatch
-
Statement type: