装的时候还是好人:给 Agent 技能做一次指纹体检

三个月前你装了一个处理 PDF 的技能,用得挺好。今天它照样能把 PDF 转成文本,只是目录里多了一个 fetch_helper.sh,SKILL.md 里多了一行从陌生 IP 拉脚本的内容。技能市场只在发布时审过一次——从那以后,这些文件就躺在你自己的磁盘上,谁都能改:上游仓库、某个自动更新脚本,或者你某次随手 git pull。skill-integrity 就是为这件事做的:它不判断内容好坏,只负责告诉你“哪些字节变了”。
为什么“装过就安全”是错觉
Agent 技能的本质就是一个文件夹:一个 SKILL.md 加上若干脚本和资源,Hermes 从 ~/.hermes/skills/ 读,Claude Code 从 ~/.claude/skills/ 读。没有任何签名校验,也没有版本锁定。安全社区把这类攻击叫 rug-pull:安装时人畜无害,等它被信任之后再悄悄改成另一副样子——加一行外连、换一个下载地址、埋一个读环境变量的脚本。
这类改动有两个特点,正好是哈希能抓的:它一定改动文件字节,而且通常只动少数几个文件。
它做的最小一件事
skill-integrity 对每个技能目录里的每个文件算 SHA-256,把结果存成一份快照(manifest),之后每次 check 都重新算一遍并对比。输出长这样:
skills scanned: 23 (snapshot taken 2026-10-10T21:44:32+0800)
[~] pdf-extractor — MODIFIED
changed: SKILL.md
added: fetch_helper.sh
1 skill(s) changed since snapshot. Review diffs before trusting them.
几个设计细节值得知道:
- 默认扫描
~/.hermes/skills和~/.claude/skills;快照里会记下这次扫了哪些目录,之后check沿用同一批目录,避免因为目录不一致而产生大量假“消失”。 - 单个文件超过 512KB 会被跳过,
__pycache__、node_modules、.git也被排除;跳过了多少个、每个技能跳了哪些,都会打印出来。 - 读不了的技能目录(权限被改)会被当作一条 finding 上报,绝不会被当成“扫描干净”——这恰恰是最值得警惕的地方。
- 退出码有语义:
0干净,1有发现,2还没有快照或用法错误。给--no-fail-on-change可以强制返回 0,方便先观察。 - 快照文件权限是 0600,全程不联网。
装法与第一次基线
官方插件目录里已经有它(skill-integrity,community 档,要求 Hermes >= 0.21.6),能搜到就直接按名字装:
hermes plugins install skill-integrity
hermes plugins enable skill-integrity
如果你的构建里目录还没同步到这一条,就按项目 README 的方式从仓库装:
git clone https://github.com/ZidoCode/skill-integrity ~/.hermes/plugins/skill-integrity
hermes plugins enable skill-integrity
装完第一件事是拍基线,否则它没有可比对的东西,会一直沉默:
/skill-integrity snapshot # 记录基线,小时级检查用它做对比
/skill-integrity check # 与基线做文件级 diff
/skill-integrity show # 看基线摘要
不想装插件也能单独用那个单文件脚本(零依赖,Python 3.8+):
python3 skill_integrity.py snapshot
python3 skill_integrity.py check --json
插件和单文件脚本的存放位置不同:插件写在 ~/.hermes/plugin-data/skill-integrity/manifest.json,独立 CLI 默认写 ~/.skill-integrity/manifest.json。想让 CLI 读插件的快照,加 --state-dir 指过去就行:
python3 skill_integrity.py --state-dir ~/.hermes/plugin-data/skill-integrity check --json
让 Agent 自己盯着
插件注册了 pre_llm_call 钩子,大约每小时跑一次检查,只在有发现时把变化的技能名注入到 agent 的上下文里。干净的时候它什么都不说,所以你既不会看到噪音,也不会假装自己有安全感。
如果你更喜欢外部定时任务,用 cron 每 12 小时跑一次并落日志也很直接:
0 */12 * * * python3 ~/skill-integrity/skill_integrity.py check --json >> ~/.skill-integrity/checks.log
配合 cron 的写入权限想清楚日志放哪、谁能读,会省掉后面很多麻烦。
它诚实的边界
这类工具最怕被当成“安全认证”,所以作者把边界写在了 README 里,我也照抄一遍:
- 哈希只能证明字节变了,不能判断意图。一次合法的上游更新和一次恶意改写,在它眼里完全一样。它给你的是触发信号和文件清单,读 diff 这件事仍然是你的一半契约。
- 它不改历史快照。攻击者如果有写权限,可以重新拍一份快照把自己的痕迹盖掉——快照是给 agent 可写的,这点必须清楚。
- 文件内部的恶意内容但字节没变?抓不到。那属于内容扫描工具的领域。
- 它不是杀毒软件,512KB 以上的大文件、符号链接都在盲区里(会被计数,但不会被追踪)。
所以正确的用法是把它当成“体检提示”,不是“安检门”。
一套能落地的节奏
- 装技能之后立刻拍基线,别等一周后再拍——那时你已经不知道哪个改动是正常的。
- 升级技能或拉取上游之后主动 check 一次,看过 diff 再重新拍基线。
- 收到 finding 时按这个顺序处理:先看 diff,确认不是自己改的;再看这个技能能碰到什么(读文件?发网络请求?读环境变量?);拿不准就先禁用它,把技能目录移走;如果它有机会读过凭据,按凭据泄露的流程轮换对应 key。
- 多 profile 环境要记住:插件用
get_hermes_home()推导技能目录和状态目录,所以非默认 profile 也会被覆盖,但每个 profile 有自己的快照。
想先从插件生态整体入手,可以看插件目录怎么用和hermes plugins 命令参考;同一位作者还有做进程清理的 janitor 和做安全评分的 ledger,思路都是“给 agent 的运行痕迹记账”。