Claude Code 的“安全存储”名不副实:Linux 版 OAuth 令牌实际以明文保存
实测 Anthropic 的终端 AI 编程工具 Claude Code(2.1.257)发现,文档中承诺的“安全存储”MCP 认证令牌,在 Linux 上只是一个权限为 0600 的明文 JSON 文件,既未使用系统钥匙串,也未做内容加密。OAuth 协议解决的是委派与续期问题,不负责令牌的本地加密保存,这两件事不应混为一谈。
文档说的“安全存储”,落盘后是什么
Claude Code 是 Anthropic 推出的终端 AI 编程工具,支持通过 MCP(模型上下文协议,一种让 AI 模型安全调用外部工具和数据的标准接口)接入外部服务。在官方 MCP 文档中,Claude Code 被描述为会“安全存储”认证令牌。
但实际检查后会发现,当用户完成多个远程 MCP 服务器的认证后,令牌并没有进入某个加密保险库,而是被写入了 ~/.claude/.credentials.json 文件,权限为 0600。
0600 表示只有当前系统用户能读写这个文件,从账号隔离角度看没有明显问题。可文件内容本身是明文 JSON,顶层的 mcpOAuth 对象直接存放了 OAuth 访问令牌。以下是审计中看到的某服务条目结构(凭据值已打码):
{
"mcpOAuth": {
"cloudflare": {
"accessToken": "...",
"refreshToken": "...",
"expiresAt": "..."
}
}
}
Anthropic 的凭据管理文档对各个操作系统的说明如下:
- macOS:优先使用系统加密钥匙串;钥匙串不可用时,回退到
~/.claude/.credentials.json。 - Linux:直接使用
~/.claude/.credentials.json,权限 0600。 - Windows:使用
%USERPROFILE%\.claude\.credentials.json,并继承用户目录的访问控制。
也就是说,文档里“安全存储”的实际覆盖范围,比一般人看到这四个字时理解的要窄得多。在 Linux 上,所谓“安全”既不代表加密,也不代表专门的密钥保管机制,而只是一层文件系统权限。
“安全存储”不等于“加密存储”
文档与实际落盘方式之间存在落差,但这本身未必是一个可被直接利用的漏洞。更值得讨论的是,这种说法可能给用户带来不切实际的安全预期。
如果 Claude Code 只是跑在一台个人 Linux 笔记本上,0600 权限通常已经能阻止其他本地用户读取。可一旦这台设备涉及备份同步、日志采集、崩溃报告打包,或曾经被其他人临时使用,那么明文 JSON 被读走的风险就会上升。开发者需要清楚这一现实,而不是被“安全存储”四个字误导。
OAuth 解决委派问题,而不是存储加密
实际使用 Claude Code 连接远程 MCP 服务器时,授权流程通常走浏览器 OAuth:
- 在终端执行
claude mcp login - 在弹出的浏览器页面点击“批准”
- 回到终端,MCP 服务器已连接
整个过程没有手动复制粘贴令牌的操作,所以很容易忽略背后的一个事实:Claude Code 确实拿到了一份凭据。只要连接需要在重启后依然有效,这份凭据就必须落盘保存。
OAuth 本身很有价值。它把授权服务器发现、作用域请求、授权码交换、访问令牌刷新和撤销等流程标准化。Anthropic 的 MCP 文档甚至让用户能限制 Claude Code 可请求的作用域。
但关键在于:OAuth 协议并不规定“本地保险库如何实现”。它管的是“令牌怎么发、怎么续期”,而不是“令牌怎么保存”。
API 令牌同样可以设置作用域、限定资源、设置过期时间、轮换甚至独立撤销。OAuth 真正带来的增量好处,是把“委派与续期”标准化,同时省去了手动复制粘贴令牌。可这些改进不会让最终落盘的令牌因为你走的是 OAuth 流程,就自动获得更强的存储安全。
简单区分:
- OAuth 解决的是:Claude Code 如何获取并续期一份委派凭据
- 密钥存储解决的是:这份凭据在两次使用之间被安置在哪里
这是两个互相独立的问题,不应该混淆。
持久化层应当支持替换
目前 Claude Code 默认把所有 Linux 用户的 MCP 令牌写入同一个明文文件,这个设计并不理想。更好的做法是把凭据持久化层做成可替换的接口,让用户或组织依据自己的安全基线,选择系统钥匙串、密码管理器、加密文件,或是云端密钥管理服务作为后端。
要实现这一点并不需要从零设计新标准。SecretSpec 就是一个现成的开源密钥解析规范,提供 Node.js / TypeScript SDK,并内置了 Rust 解析器——这意味着开发者可以基于它快速构建适配不同存储后端的接入层。
关键要点
- Claude Code 文档宣称的“安全存储”,在 Linux 上实际仅指 0600 权限的明文 JSON,没有加密,也没有钥匙串保护。
- 0600 权限能挡住普通本地用户,但备份、日志采集、第三方工具读取等场景下,明文令牌有泄露风险。
- OAuth 负责授权流程的标准化,不等于令牌的本地加密存储,不要将其混淆为一种安全存储方案。
-
更好的设计是将凭据持久化做成可替换后端,让用户根据安全需求选择钥匙串、密码管理器或加密文件。
-
Anthropic 的文档确实写明会“安全存储”MCP 凭据,但在 Linux 上,这落到实处的却是一个权限设为 0600 的明文 JSON 文件。权限位能挡住其他用户的随意读取,却挡不住 root、恶意进程或不小心留下的备份副本。
- “安全存储”实际保护到什么程度,取决于操作系统:macOS 上会优先调用系统钥匙串,Linux 上则没有任何加密存储参与。
- OAuth 解决的问题是令牌的委派与续期,它本身并不承诺也不负责本地加密保管——令牌拿到手之后怎么放,是应用层自己的事。
- Claude Code 的凭据持久化层应当做成可替换的。接入 SecretSpec 这类方案后,用户可以把凭据交给系统钥匙串、密码管理器或云 KMS,而不是被动接受默认的明文文件。
- 在 Linux 上同时管理多个 MCP 服务的 Claude Code 用户,有必要了解
.credentials.json以明文落盘这一现状,再根据自己的安全基线决定是否需要额外加密或限制访问。
更安全的备选存储方案
{
"keyStore": {
"provider": "system keyring"
}
}
把 provider 设成 "system keyring" 只是其中一个选项。团队也可以改用 OpenBao 或云厂商的 KMS 来集中管理密钥;对于没有桌面环境的 Linux 工作站,还能用 age 加密文件作为后端。无论落到哪种方案,OAuth 令牌的获取和续期流程都不受影响——变的只是凭据存放的位置,从默认的明文文件挪到了受保护的存储里。
这并非纸上谈兵。OpenAI Codex 已经走出了第一步:它把 MCP 的 OAuth 存储做成了可配置项,支持 auto、file、keyring 三个选项。用户只要把 mcp_oauth_credentials_store 设成 "keyring",就能启用系统钥匙串。这套做法离通用的密钥 provider 接口还有距离,但至少 Linux 用户不再只有“明文文件”一条路可走。
SecretSpec 协议本身也在持续迭代。0.21 版本正在开发带版本管理的解析器,以及 provider 之间的进程间通信(IPC),目标是做到零依赖集成——应用可以直接通过本地协议调用 SecretSpec 的 provider,不再需要往自己的项目里塞进 SDK 或 provider 实现。
理想状态:直接读取,而不是再造一份
SecretSpec 的长期目标可以概括成一句话:让开源工具直接从用户已有的密钥存储中读取凭据,而不是在旁边再复制出一份新的明文凭据文件。这项工作已经在多个方向落地:
- SecretSpec 已经为 Git 和 Docker 提供了凭据助手;
- 面向 Nix,SecretSpec 团队提出了通用密钥解析接口,让凭据的访问权限可以精确限制到某个具体操作;
- 由于 Git、Docker、Nix 都是开源项目,开发者可以直接审查它们的凭据读取边界,并向上游提交更安全的实现。