作者从 npm 供应链攻击与伪造安全提示出发,修补 Claude Code PreToolUse 钩子的信任缺口。案例揭示工具输出、工作区自动触发器和代理权限叠加后可能形成的新型注入风险。
前几天,我写了一篇关于 npm 蠕虫的文章。它学会了利用你对 AI Agent 的信任——这是一起与 keyv 相关的供应链攻击,却根本没费心窃取凭据。它把 hook 植入 Claude Code 和 VS Code,并安排它们在 SessionStart/folderOpen 时触发,再利用编辑器本身对 workspace 的信任引爆攻击。文章最后,我留下了一个自己也无法回答的问题:如果 workspace 信任和依赖信任都只是单一、粗粒度的决定,而 Agent 工具又不断在这层信任之上叠加新的自动触发机制,那么到了什么时候,“信任一个 workspace”会彻底失去任何明确含义?
我仍然没有完整的答案。但这周,我遇到了这个问题具体而乏味的现实版本。它烦人到让我动手修复了其中唯一一个力所能及的小切面。
前段时间,我给 Claude Code 添加了一个 PreToolUse hook——没什么特别的,它只是在执行 package 安装之前,先通过扫描器检查这些 package。测试一项无关改动时,某个工具返回了看起来十分真实的安全标注:据称有一个 package 未能通过 registry 检查,我应该警告自己,并帮助移除它。
但当时根本没有安装任何 package。所谓触发这条警告的命令只是语法检查——ast.parse 和 bash -n,仅此而已。我当场把它标记为错误信息,然后继续做别的事。但几天后,当我重新阅读自己的笔记时,才发现自己已经把它归档成了“hook 中的解析 bug,以后再修”——这个说法远没有后来查明的真相那么令人警觉。我回过头,把那条据称触发了警告的命令原封不动地放进 hook 的实际代码中重放。目标数量为零。这个 hook 甚至根本没有运行。是其他东西把一条伪造的安全警告塞进了我的工具输出,而我却差点说服自己接受一个平淡无奇的解释,没有把它当成真正的警报。
真正让我耿耿于怀的正是这一点。不是蠕虫使用的具体技术,而是这样一个事实:我就坐在那里,亲眼看着自己的 hook 触发,却无法立刻、自信地分辨“这是我的代码做的”和“这是其他东西做的,只是在冒充我的代码”。如果连我都分辨不出来,常规的依赖审查就更不可能做到。而“恶意 package 在某个传递依赖中深入三层目录,植入一份 hook 配置”,恰恰就是那篇蠕虫文章所描述的技术——没人会料到 node_modules 里还藏着可执行配置。
目前没有内置方法,可以区分“这是我有意安装的 hook”,或者“这是我让 Claude 帮我构建并安装的 hook”,与“这是恶意 package 刚刚植入的 hook”。所以我自己做了一个——claude-hookscanner。它的概念简单得堪比 MIT,尽管安装脚本最后比我预想的更长:
对你编写的每一个 hook 进行 HMAC 签名。使用一个只生成一次的共享密钥,通过 HMAC-SHA256 生成签名,并以 # sentinel-hmac: <hex> 注释的形式写入脚本本身。能够把文件写入磁盘的攻击者——这正是恶意 package 实际具备的能力——没有获取该密钥的途径。签名缺失或无效,就能可靠地说明:“这不是我签名的东西。”
对于所有无法签名的内容,则使用启发式规则——你无法对第三方 package 自己的文件进行 HMAC 签名。任何在 node_modules/ 内发现的 .claude 配置都会被标记;匹配高风险模式(curl | bash、eval、base64 -d、……)的 hook 命令会被标记;解析后指向预期 hooks 目录之外的 hook 脚本也会被标记。
为那些你确实亲自检查过并判定为无害的内容维护一份基于内容哈希的 ack-list,这样就不必在每次扫描时反复审查同一批无害的依赖残留。
再添加一个 PostToolUse 提醒:当 Agent 写入 hook 时,立刻提醒它为这个 hook 签名,免得它一直处于未签名状态,与被植入的 hook 无法区分,直到下次扫描碰巧运行。
最初促使我写下这些内容的那篇文章,本质上讲的正是要克制夸大其词的冲动。所以我要明确说明:这个工具针对的是机会主义、顺路下手的供应链蠕虫——它们是通用 payload,会在每个受害者的机器上硬编码一小批众所周知的路径,因为只有这样,才能让大规模分发 payload 的成本保持足够低。面对这类攻击,就连安装脚本采用的随机而非固定的密钥位置,也是一项真实存在但相对有限的改进——我之所以费心将其随机化,而不是使用常规、可预测的路径,原因就在这里。
它无法防御那些读过本文并且知道应该去哪里寻找密钥的定向攻击者。一个孤零零地放在 hooks 目录中、包含 64 个十六进制字符的文件,无论你给它起什么名字、把它藏在哪里,都能通过形态和位置识别出来——而且,如果主机已经被攻击者以 root 权限彻底攻陷,整套方案无论如何都会失效,因为 root 同样可以读取密钥。与其让某个人通过惨痛教训才发现“提高通用攻击的门槛”和“牢不可破”并不是一回事,我更愿意现在就把这一点说清楚。
git clone https://github.com/c0ri/claude-hookscanner.git
cd claude-hookscanner
./install.sh
注意:目前它只支持 Linux,以及 Windows 上通过 WSL 运行的 Linux。不过如果大家感兴趣,也许我可以帮忙把它移植到 mac 和真正的 Windows 上。
它会引导你完成密钥位置设置,把提醒 hook 接入 ~/.claude/settings.json(事先会创建备份),并立即运行一次扫描,让你不必仅凭信任接受它。里面也提供了 --uninstall,而且卸载时不要求你记得密钥最后被放在了哪里。
我仍然不知道,到了什么时候,“信任一个 workspace”会彻底失去任何明确含义。但至少现在,当我机器上的某个 hook 告诉我有内容未通过安全检查时,我可以要求它证明自己确实属于我——这比声称问题已经“解决”更克制,也更诚实;而这大概正是这项工具应有的分寸。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。