{"slug": "claude-code-stores-oauth-tokens-in-plaintext", "title": "Claude Code Stores OAuth Tokens in Plaintext", "summary": "Anthropic's Claude Code 2.1.257 stores OAuth tokens for remote MCP servers in plaintext JSON on Linux, protected only by file permissions, contradicting its documentation that claims tokens are 'stored securely'. The file ~/.claude/.credentials.json contains access tokens in a top-level mcpOAuth object, while macOS uses the encrypted Keychain and Windows inherits user profile access controls. The article argues that OAuth does not solve secret storage and recommends a replaceable credential-store interface, such as the SecretSpec SDK with 33 provider integrations.", "body_md": "# Claude Code Stores OAuth Tokens in Plaintext\n\nClaude Code’s [MCP documentation says authentication tokens are “stored\nsecurely”](https://code.claude.com/docs/en/mcp#authenticate-with-remote-mcp-servers).\nOn Linux, that currently means plaintext JSON protected by file permissions.\n\nI checked Claude Code 2.1.257 after authenticating to several remote MCP\nservers. The file `~/.claude/.credentials.json`\n\nhad mode `0600`\n\n, as it should,\nbut it also contained a top-level `mcpOAuth`\n\nobject with the access tokens.\nHere is the shape of one Cloudflare entry, with every credential value\nredacted:\n\nThis matches Anthropic’s [credential-management\ndocumentation](https://code.claude.com/docs/en/team#credential-management).\n\n**macOS:** uses the encrypted macOS Keychain, falling back to`~/.claude/.credentials.json`\n\nwhen the Keychain is unavailable.**Linux:** uses`~/.claude/.credentials.json`\n\nwith mode`0600`\n\n.**Windows:** uses`%USERPROFILE%\\.claude\\.credentials.json`\n\n, inheriting the access controls of the user’s profile directory.\n\nThat is a much narrower claim than most people hear when a product says a credential is “stored securely.”\n\n## OAuth did not solve secret storage\n\n[Section titled “OAuth did not solve secret storage”](#oauth-did-not-solve-secret-storage)\n\nThe browser flow makes the secret easy to miss. Run `claude mcp login`\n\n, approve\naccess in the browser, and return to a connected MCP server. Nobody manually\ncreated a token, copied it from a dashboard, or pasted it into a configuration\nfile.\n\nBut Claude Code still received a credential. It must persist that credential if the connection is to survive a restart.\n\nOAuth is valuable here. Claude Code can discover the authorization server,\nrequest specific scopes, complete the authorization-code exchange, refresh an\naccess token, and revoke the grant. Anthropic’s [MCP\ndocumentation](https://code.claude.com/docs/en/mcp#authenticate-with-remote-mcp-servers)\nalso lets users pin the scopes Claude Code requests.\n\nWhat OAuth does not specify is a secure local vault.\n\nAPI tokens can also be scoped, limited to particular resources, assigned an expiry, rotated, and revoked independently. OAuth standardizes delegation and renewal, while avoiding the copy-and-paste ceremony. Those are substantial benefits, but they do not turn the resulting bearer token into something that is safe to leave in plaintext.\n\n| Property | OAuth credential | Scoped API token |\n|---|---|---|\n| Must be stored by the client | Yes | Yes |\n| Can have limited permissions | Yes | Yes |\n| Can expire | Yes | Yes |\n| Can be revoked independently | Usually | Yes |\n| Can be replayed if stolen | Yes | Yes |\n| Standard interactive delegation | Yes | Provider-specific |\n| Standard automatic renewal | Often | Usually external |\n\nOAuth solves how Claude Code obtains and renews a delegated credential. Secret storage solves what happens to that credential between uses. They are separate concerns.\n\n## Claude Code needs a credential-store interface\n\n[Section titled “Claude Code needs a credential-store interface”](#claude-code-needs-a-credential-store-interface)\n\nClaude Code should not decide that every Linux user’s MCP tokens belong in the same plaintext file. The persistence layer should be replaceable:\n\nThe right integration point is the [SecretSpec Node.js / TypeScript\nSDK](/sdk/nodejs/). It embeds the Rust resolver, so the TypeScript side does not\nneed bespoke code for each backend. Claude Code could serialize one MCP OAuth\ncredential per server and ask SecretSpec to load, save, or delete it using the\nprovider the user or organization selected.\n\nSecretSpec 0.20 has [33 provider\nintegrations](/concepts/providers/#available-providers). They cover local\nkeyrings, password managers, encrypted files, cloud secret managers, and\ndeployment destinations. Providers declare their capabilities, so a credential\nstore can require readable and writable storage while still using the same\ninterface everywhere.\n\nThe current TypeScript SDK exposes SecretSpec’s provider-independent resolver.\nWe would add the small `get`\n\n/`set`\n\n/`delete`\n\ncredential-store surface Claude\nCode needs rather than reimplement 33 integrations in its codebase.\n\nFor example, set the [system keyring](/providers/keyring/) as the default\nprovider in the local user configuration:\n\nA team might instead require [OpenBao](/providers/openbao/) or a cloud secret\nmanager. A headless workstation might use an [age-encrypted\nstore](/providers/age/). The OAuth flow would stay exactly the same; only\npersistence would change.\n\nWhat Codex does:Codex makes MCP OAuth storage configurable. Its[configuration reference]documents`auto`\n\n,`file`\n\n, and`keyring`\n\nbackends. Setting`mcp_oauth_credentials_store = \"keyring\"`\n\nselects the system keyring. This is not a general secret-provider interface, but it avoids making a plaintext credential file the only option on Linux.\n\nComing in SecretSpec 0.21:We are working on[versioned resolver and provider IPC]for zero-dependency integrations. Applications will be able to use SecretSpec providers over a local protocol without embedding an SDK or provider code.\n\n## Making open source software more secure\n\n[Section titled “Making open source software more secure”](#making-open-source-software-more-secure)\n\nWe are working to make open source tools retrieve credentials from a\nuser-selected secret store instead of copying them into another plaintext\nfile. SecretSpec now provides [Git](/integrations/git/) and\n[Docker](/integrations/docker/) credential helpers, and we have proposed a\ngeneric, operation-scoped [secret resolver interface for\nNix](https://github.com/NixOS/nix/pull/16339).\n\nBecause these projects are open source, we can inspect their credential boundaries and contribute safer ones upstream.\n\nWe cannot make the equivalent fix in Claude Code. Its [public\nrepository](https://github.com/anthropics/claude-code) does not include the\ncore CLI implementation, and its [license is all rights\nreserved](https://github.com/anthropics/claude-code/blob/main/LICENSE.md).\nWe can document the problem and propose a SecretSpec TypeScript integration,\nbut only Anthropic can change Claude Code’s MCP OAuth storage today.\n\nThat is one of the practical security benefits of open source: when a secret crosses the wrong boundary, users do not have to wait for the vendor to decide that the boundary matters.", "url": "https://wpnews.pro/news/claude-code-stores-oauth-tokens-in-plaintext", "canonical_source": "https://secretspec.dev/blog/claude-code-stores-oauth-tokens-in-plaintext/", "published_at": "2026-09-03 01:47:44+00:00", "updated_at": "2026-09-03 02:22:03.571043+00:00", "lang": "en", "topics": ["ai-tools", "ai-safety"], "entities": ["Anthropic", "Claude Code", "Cloudflare", "SecretSpec"], "alternates": {"html": "https://wpnews.pro/news/claude-code-stores-oauth-tokens-in-plaintext", "markdown": "https://wpnews.pro/news/claude-code-stores-oauth-tokens-in-plaintext.md", "text": "https://wpnews.pro/news/claude-code-stores-oauth-tokens-in-plaintext.txt", "jsonld": "https://wpnews.pro/news/claude-code-stores-oauth-tokens-in-plaintext.jsonld"}}