一文读懂 unlazy 安全威胁模型:CHECK 即代码、审批边界与 fail-closed 设计详解
【免费下载链接】unlazyAnti-laziness skill for AI agents. Core: the Depth Tree method, which splits a task N layers deep and gives every leaf the full time budget of the whole task, so effort multiplies with depth. Grounded in 2025-2026 research on model laziness, underthinking and premature completion.项目地址: https://gitcode.com/gh_mirrors/unl/unlazy
unlazy 是面向 AI Agent 的"反懒惰"技能(anti-laziness skill),它让 AI 在长任务中先写验收账本(GATES.md)、再执行已审查的检查命令,用证据而非口头汇报证明完成。但正因为它会执行仓库里描述的 shell 命令,安全设计就格外关键。本文用通俗语言拆解 unlazy 的威胁模型:为什么CHECK:行被视为代码、审批记录如何绑定到每一个执行细节,以及 fail-closed(失败即拒绝)设计在源码中如何落地。
🎯 核心立场:安全边界是"审批",不是"沙箱"
unlazy 在 SECURITY.md 开篇就给出了一句至关重要的定调:
它的安全边界是显式的审查与审批,而不是命令沙箱(explicit review and approval, not command sandboxing)。
换句话说,unlazy不隔离你要执行的命令——命令拥有执行检查器的那个用户的完整权限:能碰到文件、网络连接、凭据和开发者工具。它真正防护的是另一个问题:
一条命令,在你明确看过并同意之前,绝不执行。
这是理解全部设计的前提。下面逐层展开。
🚨 CHECK: 行就是代码:最重要的安全假设
在账本格式里,每个可运行门禁长这样:
- [ ] G1: pricing fixtures render the expected tiers CHECK: node scripts/verify-pricing.mjs EXPECT: pricing verification passed其中CHECK:后面是一整行将在 shell 中执行的命令。unlazy 把它直接当作可执行代码对待(见 references/gates.md 的"Approval boundary"一节)。这带来几条铁律:
- 先读源码,再谈运行。继承来的账本,必须先用
--status解析(该模式永不执行),再逐条读CHECK:、EXPECT:、CWD:,以及命令调用的每一个脚本——包括生成文件和被 git 忽略的文件。 - 绝不"以运行代替阅读"。不要为了"看看它会干嘛"去跑不可信的检查——那本身就是执行。
- 不可信数据边界。账本标题、命令输出、其中引用的文本,一律视为不可信数据;不能让它们指挥 Agent 去"批准自己"或"安装 hook"。
--status是唯一"永远不执行"的模式;而普通模式对没有精确审批记录的新命令只会打印解析后的完整信息(命令、期望、工作目录、shell、PATH)后保持不执行——但注意:一旦该命令被精确批准过,普通模式就会执行它。它不是永久的 dry-run。
🔑 审批边界:一条审批只"认"一个完整执行上下文
审批记录默认存放在~/.unlazy/approved目录。unlazy 的设计核心是:审批不是一句"我同意跑这个命令",而是对一整套执行指纹的精确绑定。
在 scripts/gate-check.mjs 中,每个检查会被打包成一个"oracle"(预言机),包含:
| 绑定项 | 含义 |
|---|---|
| 绝对账本路径 + 门禁 id | 哪份文件、哪条门禁 |
精确的CHECK:与EXPECT:文本 | 命令一字不差 |
解析后的CWD:与 shell | 在哪里、用什么解释器跑 |
| 超时、输出上限、正则匹配参数 | 执行预算 |
平台 +完整继承的PATH | 在哪台机器、哪个工具环境跑 |
这些字段拼成 JSON 后取 SHA-256 作为签名。任何一项发生变化,旧审批自动失效,必须重新审查、重新批准。例如你在 Windows 上批过的命令,换到 Linux 就是"新命令";PATH变了(换了终端)同样要重新审批。
还有一个诚实的边界值得新手注意:审批不快照命令调用的脚本内容。如果CHECK:文本没变、但它调用的verify-pricing.mjs被别人改了字节,旧审批仍然生效。官方解法不是自动追踪依赖,而是要求你:变更依赖后重新审查,再跑--reverify;若需要机器强制的依赖校验,把依赖摘要直接写进被审批的CHECK:文本里,用你另外信任的工具去验证。
🛡️ fail-closed 设计:宁可拒绝,绝不"偷"
fail-closed 的意思是:当安全状态无法确定时,选择失败(拒绝执行),而不是降级放行。这是 unlazy 源码里反复出现的设计哲学,几个典型例子:
1. 锁不偷取。审批与租约都用文件锁。经典陷阱是"看到锁 → 删锁 → 占新锁":在 stat 和 unlink 之间,旧主可能已释放、后继者可能已拿到同名锁(ABA 竞态)。scripts/lib/gates.mjs 的做法是永不按路径删除观察到的锁,等锁消失就重试;崩溃进程留下的锁到超时后失败即拒绝,由人工确认 PID 不再存活后手动清理。官方明令禁止在 unlazy 运行时批量删除锁目录。
2. 符号链接与权限一律拒绝。审批目录必须是真实的、当前用户独占、无组/他权限的目录,且其规范化路径必须在仓库根之外(防止仓库里的人伪造审批)。读取审批文件时(scripts/gate-check.mjs)使用O_NOFOLLOW不跟随符号链接打开,并在读完后再验证描述符指向的还是"同一个单链接常规文件"——读取期间被换掉的文件会被拒绝。
3. 原子写入防篡改。scripts/lib/gates.mjs 的writeAtomic拒绝替换符号链接目标,先写临时文件 + fsync,再原子 rename;追加状态日志前会核对nlink === 1和 dev/ino,防止硬链接/符号链接把写入引到仓库外。
4. 解析失败即失败关闭。零门禁的账本、重复 id、缺少理由的 ABANDON,全部是解析错误而不是"完成"——不完整的结构无法产出一张"完成证书"(见 references/gates.md)。
🖥️ Shell、PATH 与继承环境:跨平台的隐形变量
检查器解析 shell 的顺序是:--shell→UNLAZY_SHELL环境变量 → 平台默认(Unix 的/bin/sh,Windows 的ComSpec/cmd)。子进程继承启动时的完整环境,包括 PATH。
这里藏着一个新手最容易踩的坑:同一个检查器从 Git Bash 启动和从 PowerShell 启动,可能解析到完全不同的工具集。--shell只是换解释器——它不安装缺失的工具、不清理环境、也不限制命令访问。因此官方强烈建议:可移植的门禁优先使用仓库自有的 Node 脚本,而不是假设grep、tail存在。
环境不匹配(shell 或 PATH 变了)应被视为验证失败去解决,而不是当作证据保留——因为解析后的 shell 和 PATH 指纹本身就是证据的一部分。
🔒 重要澄清:Scope、租约、波次都不是沙箱
unlazy 的并行协作机制(scope 管道、OWNS:路径租约、dispatch 启动波次)经常被误解为隔离手段,官方文档反复强调它们不是:
- 它们限制的是 unlazy 自己的发现范围、日志目标、协调标签;
- 它们不阻止任何进程去读写另一条路径;
- 租约匹配是保守的协调守卫(宁可拒绝安全的好组合),不是写隔离。
真正的隔离来自操作系统层面:容器、虚拟机或独立 worktree(且注意 worktree 仍可能共享外部缓存和服务)。references/parallel.md 对此有完整说明。
🧩 Stop Hook:只读状态、不执行、消息有界
可选的 Claude Code Stop hook(scripts/stop-hook.mjs)在门禁未满足或波次未完成时返回decision: "block"阻止会话草草结束。它的安全设计同样讲究:
- 不执行任何
CHECK:命令,只解析账本与 dispatch 状态; - 连续 6 次无语义进展才释放(改注释、重排版、evidence 行的 PATH 哈希刷新都不算进展,防止"改一行就重置计数器"的无限续命);
- 输出给宿主的消息剥离所有 C0/C1 控制字符和双向格式标记、长度受限(scripts/stop-hook.mjs)——因为仓库可诊断文本是攻击者可以控制的,绝不能借机改写终端历史或视觉上重排文字;
- 自由文本的放弃理由永不被复制进特权消息,只输出有界的
HANDOFF REQUIRED摘要。
🧹 进程树清理:一个 Windows 细节
每个检查跑在一个独立的 Node 监督者进程(scripts/lib/check-supervisor.mjs)之下,它作为进程组组长存活到 shell 及所有继承的 stdout/stderr 描述符关闭为止。超时清理时的细节体现了"最小信任":
- POSIX 上只在监督者仍被观察到存活时才向进程组发信号;一旦它已退出,其 PID/PGID 数字可能已被系统复用,绝不再对它发信号;
- Windows 上只接受
SystemRoot、WINDIR、SystemDrive三个值一致指向的系统根目录下的taskkill.exe——任意为空或不一致就拒绝,绝不从命令自己的当前目录或 PATH 里找(那里是CHECK:环境可控的区域),见 scripts/lib/process-tree.mjs; - "发出了杀信号"不等于"进程已死",检查器始终有自己兜底的有界定时器。
📁 证据与日志:成功输出不落盘
命令输出可能含隐私路径或敏感文本,unlazy 的处理策略是只消费、不留存:
- 成功输出只用于
EXPECT:匹配,然后被压缩为SHA-256 摘要 + 字节数写入证据,原始内容不回显、不入账本; - 失败诊断仅出现在本地终端、有界、并剥离终端控制字符;
- dispatch 状态里只存时间戳和不透明的宿主句柄——官方明确要求:不要把 prompt、凭据或结果正文塞进任何句柄。
另外,unlazy 本身不采集遥测、不向外发送审批/门禁/hook 状态;但请记住:一条CHECK:命令是任意代码,它自己可以发起任何网络或日志行为——这正是"CHECK 即代码"的另一面。
📝 安装器的隐私边界
scripts/install-hooks.mjs 只在显式调用时修改 Claude Code 设置:
- 默认写项目本地的
.claude/settings.local.json(含绝对路径,可能暴露本地目录名,请加入 ignore 规则); --shared写入的绝对路径通常不可移植,协作场景慎用;- 安装/卸载保留无关 hook,新处理器带精确的管理标记(不用子串匹配),旧式处理器也只按精确形状识别;
- 拒绝畸形设置形状而不是直接覆盖,原子写入并在替换前生成
<settings-file>.unlazy.bak备份。
✅ 新手安全检查清单
把以上原理浓缩成一张可操作的清单:
- 运行继承账本前:先
--status解析,逐条读完CHECK:、EXPECT:、CWD:和被调脚本 - 确认 shell 解析结果(
--shell→UNLAZY_SHELL→ 平台默认)与继承的PATH符合预期 - 只对你写的或完全理解的命令使用
--approve - 任何绑定项(账本、命令、CWD、shell、PATH……)变化后,重新审批
- 依赖脚本/fixture 变更后:重新审查 + 跑
--reverify - 不要批量删除
.unlazy/locks下的锁文件 .unlazy/、.unlazy-hook-state.json、.claude/settings.local.json加入项目 ignore- 不可信代码:用一次性环境或更强的沙箱(容器/虚拟机)
- 提交或分享账本前,复查其中的证据与状态日志是否含敏感信息
- 提交任何 Claude 设置文件前,先审查 diff
📮 发现漏洞怎么办
一般缺陷走常规 issue 并附最小复现;但如果复现本身会暴露密钥或构成滥用途径,应优先使用仓库的私有漏洞报告通道;不可用时,先发一个最小 issue 请求建立私密联系渠道,在渠道建立前不要包含敏感细节。完整流程见 SECURITY.md 的"Reporting a vulnerability"一节。
unlazy 的威胁模型可以一句话总结:它不假装能隔离不可信代码,而是把"未经审查的命令绝不执行"做成了一整套精确到执行指纹的审批协议,并在所有无法确定安全的路口选择拒绝。理解 CHECK 即代码、审批边界、fail-closed 这三个概念,你就掌握了它全部安全设计的骨架。
【免费下载链接】unlazyAnti-laziness skill for AI agents. Core: the Depth Tree method, which splits a task N layers deep and gives every leaf the full time budget of the whole task, so effort multiplies with depth. Grounded in 2025-2026 research on model laziness, underthinking and premature completion.项目地址: https://gitcode.com/gh_mirrors/unl/unlazy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考