装的时候还是好人:给 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 以上的大文件、符号链接都在盲区里(会被计数,但不会被追踪)。

所以正确的用法是把它当成“体检提示”,不是“安检门”。

一套能落地的节奏

  1. 装技能之后立刻拍基线,别等一周后再拍——那时你已经不知道哪个改动是正常的。
  2. 升级技能或拉取上游之后主动 check 一次,看过 diff 再重新拍基线。
  3. 收到 finding 时按这个顺序处理:先看 diff,确认不是自己改的;再看这个技能能碰到什么(读文件?发网络请求?读环境变量?);拿不准就先禁用它,把技能目录移走;如果它有机会读过凭据,按凭据泄露的流程轮换对应 key。
  4. 多 profile 环境要记住:插件用 get_hermes_home() 推导技能目录和状态目录,所以非默认 profile 也会被覆盖,但每个 profile 有自己的快照。

想先从插件生态整体入手,可以看插件目录怎么用和hermes plugins 命令参考;同一位作者还有做进程清理的 janitor 和做安全评分的 ledger,思路都是“给 agent 的运行痕迹记账”。