Store API credentials in the OS secret store instead of plaintext
Proposal: Store API credentials in the OS secret store instead of plaintext
Summary
Claude Code currently persists API credentials (Anthropic API keys, OAuth tokens, and any third-party tokens picked up via MCP server configs) to plaintext files on disk (e.g. under ~/.claude/ / ~/.config/claude-code/). This proposal asks that Claude Code adopt OS-native secret storage as the default credential backend, with plaintext file storage kept only as an explicit opt-in fallback for headless/CI environments where no secret store is available.
This mirrors a change the Rust toolchain shipped for Cargo: cargo login now supports a global-credential-providers config that routes registry tokens through cargo:libsecret (Linux/Secret Service), and equivalent providers exist for macOS Keychain and Windows Credential Manager, instead of writing ~/.cargo/credentials.toml in plaintext. That change is a useful precedent for scope and rollout shape.
Problem
- Any plaintext credential file is readable by any process running as the same user — no additional compromise is required beyond arbitrary local code execution (a malicious npm/pip/cargo package, a compromised editor extension, etc.).
- Backup tools, dotfile-sync tools, and cloud drive sync (Dropbox, iCloud, OneDrive) commonly sweep up home-directory config files, meaning API keys can end up replicated to third-party storage without the user realizing it.
- Shell history, crash dumps, and
tar/cp -rof a home directory during support/debugging can leak the file contents inadvertently. - Anthropic API keys are billable credentials — leakage has direct financial impact, not just a security-hygiene concern.
- Many developers already rely on OS secret stores for other CLI tools (
gh,aws configure sso,docker loginwithcredsStore,cargo loginas above,npmvia keytar-based wrappers) — a plaintext credential file is now the exception rather than the norm for developer tooling.
Proposed design
Backends
Add a pluggable credential-storage layer with OS-native backends:
| Platform | Backend |
|---|---|
| macOS | Keychain (Security.framework via security CLI or a native binding) |
| Linux | Secret Service API (libsecret — GNOME Keyring, KWallet, KeePassXC's Secret Service shim) |
| Windows | Credential Manager (wincred) |
A single credential is a namespaced entry, e.g. service = claude-code, account = api-key or oauth:<provider> or mcp:<server-name>, mirroring how the current config keys credentials.
Fallback
Not every environment has a secret store available (headless Linux boxes, containers, CI runners, WSL without a keyring daemon). In those cases:
- Detect the absence of a usable Secret Service / Keychain / Credential Manager at startup.
- Fall back to the existing plaintext file, but emit a one-time warning (not on every invocation) that credentials are stored unencrypted and suggest installing/enabling a keyring (e.g.
gnome-keyring,kwalletd, or a headless option likekeyctl/pass). - Support an explicit escape hatch, e.g.
CLAUDE_CODE_CREDENTIAL_STORE=file, for users who intentionally want file-based storage (containers with a mounted secrets volume, etc.) without the warning noise.
Migration
- On first run after the change ships, if a plaintext credential file exists and a secret store is available, migrate the credential into the secret store and delete (not just leave) the plaintext copy, with a printed confirmation of what happened.
- If migration fails for any reason (locked keyring, permission issue), leave the plaintext file untouched and surface a clear error rather than silently losing the credential.
Config surface
- A config key (e.g.
credentialStore: "auto" | "keychain" | "file") analogous to Cargo'sglobal-credential-providers, defaulting to"auto"(try OS store, fall back to file with a warning). - Respect this setting per-machine, not per-project, since it reflects local OS capability rather than project policy.
Non-goals
- This does not attempt to solve credential storage for remote/headless agent execution (e.g. Claude Code running inside a CI job or a cloud sandbox) — those environments legitimately need injectable, ephemeral credentials (env vars, short-lived tokens) rather than a local keyring, and should continue to support
ANTHROPIC_API_KEY-style env var injection unchanged. - This does not change how MCP servers themselves store credentials internally — only how Claude Code stores the credentials it holds on the user's behalf to talk to those servers/APIs.
Suggested implementation library
@napi-rs/keyring — a Node.js binding (via napi-rs) around the Rust keyring-rs crate, the same crate that backs Cargo's own cargo:libsecret / cargo:macos-keychain / cargo:wincred credential providers referenced below. It covers all three target backends (macOS Keychain, Windows Credential Manager, Linux Secret Service) without a build-time libsecret dependency, which keeps it friendly to headless/WSL2 environments and lines up directly with the fallback behavior described above. It supersedes the now-archived keytar, off of which Microsoft's own @azure/identity-cache-persistence and MSAL libraries are actively migrating for the same reason.
Prior art
- Cargo:
cargo:libsecret/cargo:macos-keychain/cargo:wincredcredential providers, configured viaregistry.global-credential-providersinconfig.toml. - Docker:
credsStore/credHelpersin~/.docker/config.json, backed bydocker-credential-osxkeychain,docker-credential-secretservice,docker-credential-wincred. - GitHub CLI (
gh): stores auth tokens in the OS keyring by default, withGH_TOKEN/plaintext fallback documented explicitly as the less-preferred path.
Ask
Requesting that the Claude Code team implement this directly.