Claude Code OAuth Tokens Stored in Plaintext

Anthropic's documentation says MCP authentication tokens are "stored securely." On Linux, that means a plaintext JSON file at ~/.claude/.credentials.json with mode 0600. That's it. No encryption. No system keyring. Just file permissions — the same protection your SSH keys got in 1995.

Security researcher Domen Kožar published the finding yesterday for Claude Code 2.1.257, and it's worth understanding why this matters beyond the headline. The platform gap is the real story: macOS users get encrypted Keychain storage; Linux users get a text file.

The Finding: What's in ~/.claude/.credentials.json

After authenticating to several remote MCP servers via claude mcp login, Kožar inspected ~/.claude/.credentials.json. The file is mode 0600 (owner read/write only) — correct permissions, but the contents are entirely unencrypted:

{
  "mcpOAuth": {
    "cloudflare-observability|…": {
      "accessToken": "<redacted>",
      "clientId": "<redacted>",
      "discoveryState": "<redacted>",
      "redirectUri": "<redacted>",
      "serverName": "cloudflare-observability",
      "serverUrl": "<redacted>"
    }
  }
}

The OAuth flow is smooth — click a link in the terminal, approve in the browser, return to a connected MCP server. Nobody manually created a token or pasted anything. That convenience masks the fact that Claude Code still received a credential it must persist, and on Linux the persistence layer is a flat file.

Anthropic's own credential-management documentation confirms the split:

The Platform Gap: Why Linux Gets Less

This isn't a bug. It's a design decision. macOS ships with a system keyring (Keychain) that provides encrypted credential storage with a standard API. Linux has libsecret / D-Bus Secret Service, GNOME Keyring, KDE Wallet, and a dozen other solutions — none of them universal.

The problem is that "no universal solution" on Linux became "no solution at all." Instead of shipping with libsecret support (which works across GNOME, KDE, and most desktop environments), or integrating with a cross-platform secret-storage abstraction, Claude Code defaults to a plaintext file.

graph LR
    A[Claude Code MCP Login] --> B[OAuth Browser Flow]
    B --> C{Platform?}
    C -->|macOS| D[Encrypted Keychain]
    C -->|Linux| E[~/.claude/.credentials.json]
    C -->|Windows| F[%USERPROFILE%\.claude\.credentials.json]
    D --> G["Stored securely ✓"]
    E --> H["Mode 0600 plaintext JSON ✗"]
    F --> I["Profile ACL plaintext JSON ✗"]
            

Mode 0600 protects against casual shoulder-surfing and other users on a shared machine — assuming they don't have root. But any process running as the same user, any compromise of a background agent, any automated tool that reads ~/.claude/ — all of them get the full OAuth token store. The barrier between "you" and "your tokens" is a file permission bit.

OAuth Didn't Solve Credential Storage

This is the subtle point that's easy to miss. OAuth solves delegation — how Claude Code obtains and renews a scoped credential without you typing a password. It does not solve storage — what happens to that credential between uses. These are separate concerns, and Anthropic conflated them.

The browser-based login flow makes the problem invisible. You never see a token string, never copy-paste anything. But the same OAuth access token that survives a restart is sitting in a file, ready to be read by anything running as you. A scoped token that can access your Cloudflare resources, your GitHub MCP servers, or your internal APIs — all in one JSON file.

As Kožar puts it: "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."

The Fix: What a Credential-Store Interface Looks Like

Kožar's proposal is a credential-store interface using SecretSpec, a specification and TypeScript SDK that abstracts secret storage across 33 providers — local keyrings, password managers, encrypted files, cloud secret managers, and deployment destinations.

Claude Code OAuth client
        │
        ▼
credential-store interface
        │
        ▼
SecretSpec TypeScript SDK
        │
        ▼
User-selected SecretSpec provider

The interface would be small: get, set, delete — one credential per MCP server. The user or organization selects a provider (system keyring, Bitwarden, 1Password, AWS Secrets Manager, whatever fits their threat model), and Claude Code uses it transparently. The current behavior becomes the fallback — not the default.

This isn't theoretical. SecretSpec 0.20 already has 33 provider integrations and a TypeScript SDK that embeds a Rust resolver. The integration work is writing a thin adapter, not building a vault from scratch.

Bottom Line

Claude Code is a tool that runs code from LLM suggestions — often with elevated privileges. Its credential store should be a hardened vault, not a file. The macOS/Linux asymmetry is telling: when a platform provides a standard encrypted storage API, Anthropic uses it. When it doesn't, they punt. For a tool that's increasingly positioned as autonomous agent infrastructure, "mode 0600" is not a security strategy. It's a default that will become a CVE.