Claude Code 的“安全存储”名不副实:实测 Linux 版 OAuth 令牌以明文 JSON 落盘
摘要:实测 Anthropic 终端 AI 编程工具 Claude Code 2.1.257 发现,文档中承诺的 MCP 认证令牌“安全存储”,在 Linux 上只是一个权限为 0600 的明文 JSON 文件——没有系统钥匙串参与,也没有内容加密。本文梳理文档与实际的落差,澄清 OAuth 与本地密钥存储是两个被混淆的问题,并给出更合理的落盘设计思路。
先说背景:Claude Code 怎么拿到这些令牌
Claude Code 是 Anthropic 推出的终端 AI 编程工具,开发者可以通过它让 AI 直接调用外部服务。这类交互走的是 MCP——模型上下文协议,作用是提供一个标准接口,让 AI 模型在用户授权的前提下调用外部工具和数据。而本文要讨论的,正是这套流程产生的认证令牌最终被存到了哪里。
按官方 MCP 文档的说法,Claude Code 会“安全存储”认证令牌。但实际上,当用户完成多个远程 MCP 服务器的认证后,令牌并没有进入什么加密保险库,而是被整齐地写进了 ~/.claude/.credentials.json 文件。
“安全存储”落盘后,实际是什么形态
以 Linux 为例,这个文件权限为 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 被读走的风险就会明显上升。对依赖终端 AI 编程工具的开发者来说,这一点尤其值得留意:集成的外部服务越多,本地散落的凭据就越多,越有必要搞清楚它们到底是以什么形式保存的——而不是被“安全存储”四个字带着走。
OAuth 解决“委派”,不解决“存储加密”
实际连接远程 MCP 服务器的流程通常是这样的:
- 在终端执行
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 解析器——开发者可以基于它快速构建适配不同存储后端的接入层,而不是把一个明文 JSON 文件当作终点。
关键要点
- Claude Code Linux 版(2.1.257)的 MCP O
摘要:Claude Code 的文档承诺“安全存储”MCP 凭据,但在 Linux 上,这个所谓的安全存储,仅仅是权限设为 0600 的明文 JSON 文件——既没有加密,也不调用系统钥匙串。这种设计挡得住普通用户随意查看,却挡不住备份泄露、恶意进程读取等更多风险场景。
0600 权限能防谁,防不了谁
Claude Code 在 Linux 上保存 OAuth 令牌的实际做法,是写一个权限为 0600(仅属主可读写)的 JSON 文件。这个权限位能阻止其他普通用户直接读取,但也仅此而已。
危险并不只来自“其他用户”。你的备份系统会把文件原样复制走,日志采集工具可能读到内容,任何一个以你的用户权限运行的第三方脚本都能打开它。若攻击者拿到的是 root 权限,0600 更是形同虚设。
“安全存储”的程度取决于操作系统
Claude Code 的透明与不透明,在不同平台上的表现迥然不同:
- macOS 上,Claude Code 会优先调用系统钥匙串来保存凭据,钥匙串本身有加密保护;
- Linux 上,则没有任何加密组件参与——凭据直接以明文落盘,系统级的密钥管理器完全不介入。
也就是说,同一份文档里写的“安全存储”,在不同系统上完全是两码事。对 Linux 用户而言,这只是文档描述与现实之间的偏差。
OAuth 不负责“存储”,请不要混淆
OAuth 协议解决的是授权流程的问题——它定义了指派机制、刷新流程和撤销方式,确保第三方应用能安全地委派访问权限。至于拿到的令牌在本地怎么存放,OAuth 完全不涉及,这是应用层自己的职责。
某种意义上看,Claude Code 在 Linux 上的默认行为,就是把本地安全防线全押在文件权限上。文档用“安全存储”这种字眼,容易让人误以为它启用了加密或钥匙串,而实际并非如此。
凭据存储应当可替换
理想的设计,应当把凭据的持久化层做成可插拔的后端。用户在配置里指明凭据存到系统钥匙串、密码管理器、还是加密文件,而不是被默认实现绑死在明文文件上。
{
"keyStore": {
"provider": "system keyring"
}
}
把上面的 provider 设成 "system keyring",只是选项之一。团队也可以改用 OpenBao 或云厂商的 KMS 来集中管理密钥;对于没有桌面环境的 Linux 工作站,age 加密文件同样可行。无论选哪种,OAuth 令牌的获取、刷新和撤销流程都不受影响——变的只是“令牌放哪”这一层。
已有先例:OpenAI Codex 迈出了第一步
开源社区已经有人在做这件事。OpenAI Codex 将 MCP 的 OAuth 存储配置化,目前支持三种选项:
auto——由程序自行判断;file——明文文件保存;keyring——调用系统钥匙串。
用户只要把 mcp_oauth_credentials_store 设成 "keyring",令牌就会存入系统钥匙串。这虽然还不是完全通用的密钥 provider 接口,但对 Linux 用户意味着不再只有“明文文件”一条路可选。
SecretSpec 协议仍在推进
SecretSpec 也因此仍在持续迭代。其 0.21 版本正在开发带版本管理的解析器,以及 provider 之间的进程间通信,目标是做到零依赖集成——应用可以直接通过本地协议调用 SecretSpec provider,无需把 SDK 或 provider 的实现代码塞进自己的项目里。
最终目标:直接读,不复制
SecretSpec 的长期目标,可以概括为一句话:让开源工具直接从用户已有的密钥存储中读取凭据,而不是在本地再造一份明文副本。
这项工作已经在三条线上推进:
- SecretSpec 已经为 Git 和 Docker 提供了凭据助手;
- 面向 Nix 的开发中,SecretSpec 提出了通用密钥解析接口,让凭据访问权限可以精确限制到某个具体操作;
- 由于 Git、Docker、Nix 都是开源项目,开发者可以审计它们的凭据读取边界,并向上游提出更安全的实现方案。