planning-with-files 计划文件锁定:/plan-attest 与 SHA-256 防篡改证明机制深度解析
【免费下载链接】planning-with-filesPersistent file-based planning for AI coding agents and long-running tasks. Crash-proof markdown plans, session recovery after /clear and compaction, per-turn re-injection against context rot, deterministic completion gate. Manus-style. Install from npm, the Claude Code plugin marketplace, or npx skills. Codex, Cursor, OpenCode, 60+ agents.项目地址: https://gitcode.com/GitHub_Trending/pl/planning-with-files
plan-attest 是 planning-with-files 中负责"锁定计划文件"的核心命令:它将当前task_plan.md的内容计算为 SHA-256 摘要并落盘保存,此后每一次 hook 注入计划内容前都会重新比对摘要,一旦发现文件被静默改动,立即停止注入并抛出[PLAN TAMPERED — injection blocked]警告。本文以 commands/plan-attest.md 为骨架,结合 scripts/attest-plan.sh、scripts/inject-plan.sh 与 tests/test_plan_attestation.py 的源码实现,完整讲解该命令的解析顺序、三种工作模式、hook 校验链路、原子写入与并发安全,以及并行会话下的推荐用法。读完本文,你将能够熟练使用/plan-attest锁定与解锁计划、正确理解"证明"的信任边界,并在多会话并行场景下落地安全的计划管理流程。
为什么要锁定计划文件:防篡改的动机
planning-with-files 的核心机制是"逐轮将计划文件内容重新注入模型上下文"。这意味着模型每一轮实际"看到"的task_plan.md内容,直接决定了它接下来会执行什么。如果task_plan.md在未经人工确认的情况下被悄悄改写——例如外部工具写入、脚本误操作、会话恢复时合并出错——那么注入给模型的就是一份未经批准的"新计划",可能把整个任务带偏,甚至让 Agent 执行攻击者注入的指令。
plan-attest 解决的就是这个问题:先把"已批准"的计划内容用 SHA-256 固定下来,之后 hook 在每次注入前都做一次"当前内容 vs 已批准摘要"的比对。内容变了,就拒绝注入。这一设计是该项目防上下文腐烂(context rot)与防注入(injection)体系中的重要一环,相关背景还可参考 docs/attestation-locking.md 与 docs/agent-forgets-plan-after-clear.md。
命令速览:能力、可用性与调用边界
/plan-attest的命令定义位于 commands/plan-attest.md,其 frontmatter 明确记录了关键信息:
- 能力描述:锁定当前
task_plan.md的内容(SHA-256 证明)。hook 若发现文件摘要与已存证明不一致,将拒绝注入计划内容,从而阻断静默篡改;--show用于打印已存摘要,--clear用于移除证明。 - 可用版本:自 v2.37.0 起提供(README 与 CHANGELOG.md 可查证)。
disable-model-invocation: true:该命令不允许模型自行调用,必须由用户在 Bash 中手动执行——这本身就是一道"人工确认"闸门,防止 Agent 自我背书。allowed-tools: "Bash":命令经由 Bash 工具执行。
运行一次/plan-attest的标准操作序列(原文档定义):
- 解析活动计划:依次优先使用
${PLAN_ID}环境变量、.planning/.active_plan文件、最新的.planning/<dir>/目录;若当前目录本身就是合法的.planning/<slug>/目录,则直接使用其中的task_plan.md;否则回退到遗留模式的./task_plan.md。 - 计算摘要:对解析到的
task_plan.md计算 SHA-256。 - 写入证明:将十六进制摘要写入
.planning/<active-plan>/.attestation(并行计划模式)或./.plan-attestation(遗留模式)。 - 向用户确认:打印摘要前 12 位短哈希与存储路径。
关键安全约束:显式指定的${PLAN_ID}或${PWF_PLAN_ROOT}若无法解析,命令将以错误退出,绝不回退到其他计划。
计划解析:五级优先级与"绑定即不回退"原则
解析逻辑完整实现在 scripts/attest-plan.sh 的resolve_plan_file()(第 44-74 行)中,顺序如下:
${PLAN_ID}环境变量 →./.planning/$PLAN_ID/;./.planning/.active_plan指向的计划目录;- 按 mtime 最新的
./.planning/<dir>/; - 当前目录本身是合法的
.planning/<valid-slug>/(由resolve_from_slug_cwd()与slug_is_valid()校验,第 25-42 行); - 遗留模式:项目根目录的
./task_plan.md。
两个值得注意的实现细节:
- slug 合法性校验:
slug_is_valid()只允许A-Za-z0-9._-字符,拒绝空字符串,防止路径注入或指向任意目录。 - 显式选择器是"绑定"而非"提示":如果共享解析器
resolve-plan-dir.sh拒绝了显式选择器,attest-plan.sh会直接返回失败(第 54-58 行),而不是通过 cwd 回退去证明另一个计划。脚本针对这种情况给出了带原因的报错信息(第 116-123 行):PLAN_ID=xxx names no plan directory under .planning...或PWF_PLAN_ROOT=xxx did not resolve to a project root...。这样做的历史原因写在源码注释里:在修复之前,一个拼错的PLAN_ID会以 rc=0 错误地证明另一个计划。
证明存储位置:并行模式与遗留模式的差异
存储路径由attestation_path_for()决定(scripts/attest-plan.sh):
- 遗留模式(计划文件在项目根目录,即
task_plan.md的父目录为.):摘要写入./.plan-attestation; - 并行计划模式(
.planning/<slug>/下的计划):摘要写入该计划目录内的.attestation。
测试 tests/test_plan_attestation.py 中的test_parallel_plan_attest_writes_into_plan_dir明确断言:当存在活动计划目录时,必须写入计划目录内的.attestation,且不得在根目录残留遗留的.plan-attestation文件;test_attest_from_inside_plan_dir_updates_slug_attestation进一步验证了从 slug 目录内部直接调用也不会创建遗留文件。
三种工作模式:attest / --show / --clear
命令参数解析位于 scripts/attest-plan.sh,支持三种模式:
| 模式 | 触发方式 | 行为 |
|---|---|---|
| attest(默认) | 无参数 | 计算task_plan.md的 SHA-256 并原子写入证明文件,随后打印前 12 位短哈希与存储路径 |
| show | --show | 打印当前存储的完整摘要、计划文件路径、证明文件路径;若存在.nonce(会话随机数)一并打印 |
| clear | --clear | 删除证明文件,重新"解锁"计划以允许编辑 |
show:查看当前锁定状态
--show分支(第 128-145 行)输出:
Plan: task_plan.md Attestation: ./.plan-attestation SHA-256: <64 位十六进制摘要> Nonce: <若存在>其中 Nonce 来自init-session为每个计划生成的.nonce文件(源码注释将其标注为安全项 A1.4),hook 会用它构造带碰撞保护的 BEGIN/END 分隔符;show 仅作信息展示。若尚未设置证明,命令输出No attestation set for ...并以非零码退出。
clear:解除锁定
--clear分支(第 146-153 行)删除证明文件。这通常在有意编辑并重新批准计划之前使用——先解锁、编辑、再用/plan-attest重新锁定。测试test_clear_removes_attestation验证了清除后文件确实消失。
attest:核心锁定流程
默认模式(第 154-251 行)包含四个关键环节,下面分别展开。
摘要计算与 Hook 校验链路:TAMPERED 是如何触发的
摘要计算
compute_hash()(scripts/attest-plan.sh)优先使用sha256sum,否则回退shasum -a 256,两者都不可用时明确报错退出。
Hook 侧校验
锁定之后,真正执行"注入前比对"的是 scripts/inject-plan.sh。每次 UserPromptSubmit 与 PreToolUse hook 触发时:
- 确定证明文件:遗留模式取
${PLAN_PREFIX}.plan-attestation(第 532 行),并行模式取${RESOLVED}/.attestation(第 539 行); - 重算当前摘要:对计划文件实时计算 SHA-256,并去掉 GNU coreutils 可能添加的转义反斜杠前缀(第 1100-1106 行)。实现注释特别强调:mtime 与缓存摘要都不是可信信号,因为文件可以在保持两者不变的情况下被改写,所以每次触发都全量重算;
- 比对并分支:
- PreToolUse 场景(第 1137-1138 行)与 UserPromptSubmit 场景(第 1163-1169 行)都会输出
[planning-with-files] [PLAN TAMPERED — injection blocked]; - 在用户提示词场景下还会附上
expected=<存储摘要>与actual=<实际摘要>供排障,并提示:Run /plan-attest to re-approve current contents, or restore the file from git.
- PreToolUse 场景(第 1137-1138 行)与 UserPromptSubmit 场景(第 1163-1169 行)都会输出
也就是说,"每次有意编辑并重新批准计划后都要重新运行/plan-attest"——这正是原文档结尾给出的操作准则。
防检查-使用竞态的快照机制
校验并非"先读文件再比较"的朴素实现。scripts/inject-plan.sh 先把计划文件复制成私有快照,证明校验针对快照的精确字节进行,之后所有输出也只读快照。即使攻击者在读后恢复原始 mtime,也无法制造 check-then-use 的时间窗口;快照复制本身通过 O_NOFOLLOW 等受限描述符完成(safe_snapshot(),第 600-619 行)。
信任边界:证明 ≠ 签名,必须清醒认识
docs/attestation-locking.md 明确划定了这一机制的信任边界,引用时务必如实传达:
- 存储的值只是本地普通摘要,不是密钥签名,也不是"人类已批准"的证明;它仅在摘要本身保持可信的前提下才能检测计划变更。
- 一个能同时改写
task_plan.md及其证明文件的进程,可以让新内容顺利通过校验。 - 初始化过程中的自动证明记录的是生成时的字节,不包含独立的人工复核步骤。
- 证明不意味着计划内容可以无条件服从:即使文件与摘要匹配,从工具、网站或其他外部来源复制进来的指令仍应视为不可信内容。若要抵御能控制整个计划目录的写入者,需要独立的信任边界(例如用权限保护审批记录)。
这是整个 attestation 设计中"诚实且安全"的关键部分:它防的是静默篡改,不是万能防篡改。
写入路径的原子性与并发安全
证明文件的写入不是简单重定向,而是"临时文件 + 原子重命名 + 可选 flock"(scripts/attest-plan.sh):
- 先将摘要写入临时文件
attestation_file.tmp.$$; - 若有
flock,在flock -w 5内执行mv -f(锁文件为证明文件同目录下的.attestation.lock),否则直接原子重命名; - 读回校验:写入完成后读取磁盘上的证明文件,与期望摘要比对(第 233-244 行)。不一致即报错并以非零码退出,杜绝"宣称已锁定、实际未落盘"的陈旧证明被信任;
- 失败回退:若首次重命名失败(跨设备、权限等),会通过第二次原子重命名补齐,绝不裸重定向到活文件(那会让并发校验者读到撕裂的中间状态)。
原子重命名是正确性保证——读者永远不会看到半截摘要;flock 只是协同并发写入者的"合作闸门"。这个设计对应 v2.40 的一个真实回归:并发遗留模式会话曾因非原子> file重定向产生截断的证明文件,导致 hook 误报 TAMPERED。回归测试test_concurrent_attest_writes_do_not_corrupt_file(tests/test_plan_attestation.py)并发拉起 8 个 attest 进程,断言最终文件始终是完整的 64 位十六进制摘要且与计划内容一致。
另外,attest 分支还会检测"遗留模式下证明文件在 30 秒内被其他进程改写过"并给出提示,建议并行会话改用 slug 模式(第 169-192 行)。
跨平台行为与 Windows 实现
flock的可用性随平台而异,docs/attestation-locking.md 给出了对照:
| 平台 | flock可用性 | 行为 |
|---|---|---|
| Linux | 通常可用 | 原子重命名 + 协同flock守卫 |
| macOS | 默认未安装 | 原子重命名仍保证正确性 |
| Windows Git Bash | 通常缺失 | 原子重命名仍保证正确性 |
| WSL | 通常可用 | 与 Linux 相同 |
Windows 原生安装使用 PowerShell 实现 scripts/attest-plan.ps1(要求 PowerShell 5.0+),通过-Show/-Clear开关对应--show/--clear。其实现值得注意:它内嵌了 Win32 P/Invoke(PwfAttestationNative,第 38-60 行),以受限句柄完成无跟随(no-follow)的文件操作,并在非 Windows 主机上直接抛错要求改用attest-plan.sh。
命令的完整调用方式(原文档给出的实现路径):
# Linux/macOS/Git Bash(插件安装) sh ${CLAUDE_PLUGIN_ROOT}/scripts/attest-plan.sh # Windows PowerShell(插件安装) & "$env:CLAUDE_PLUGIN_ROOT\scripts\attest-plan.ps1" # Windows PowerShell(独立安装) & "$env:USERPROFILE\.claude\skills\planning-with-files\scripts\attest-plan.ps1"v3 模式下的强制证明(autonomous / gated)
attestation 在遗留模式(无.mode文件)是可选的,但在 v3 的autonomous与gated模式下是强制的。scripts/inject-plan.sh 中的逻辑(安全项 security-major-4)说明:在无人值守的循环中,计划体会在每一轮注入模型;仅靠 nonce 分隔符无法防御分隔符混淆注入——因为.nonce与task_plan.md处于同一信任域,能写计划的人就能读到 nonce 并伪造 END 分隔符。因此证明才是真正的防线:
- 若
autonomous/gated模式下不存在证明(ATTEST为空),计划体不得注入,只输出一行提示:[planning-with-files] v3 mode requires attested plan; run attest-plan; - PreToolUse 与 UserPromptSubmit 两个上下文都执行该强制检查。
并行会话的最佳实践:slug 模式 + PLAN_ID 固定
遗留模式(./task_plan.md+./.plan-attestation)下多个会话共享同一份计划与证明文件,原子重命名虽能保证证明文件有效,却无法让共享计划文件成为安全的并行工作区。docs/attestation-locking.md 推荐的并行方案是 slug 模式:
./scripts/init-session.sh "Backend Refactor" ./scripts/init-session.sh "Incident Investigation"每个 slug 拥有完全隔离的文件:
.planning/2026-01-10-backend-refactor/task_plan.md .planning/2026-01-10-backend-refactor/.attestation .planning/2026-01-10-incident-investigation/task_plan.md .planning/2026-01-10-incident-investigation/.attestation需要把某个终端固定到指定计划时,导出PLAN_ID再执行证明:
export PLAN_ID=2026-01-10-backend-refactor sh scripts/attest-plan.shslug 模式通过"每会话独立task_plan.md+ 独立.attestation"从根本上避免同文件竞争。
测试如何保障行为契约
tests/test_plan_attestation.py 是一份完整的契约清单,可用于自行复现验证(跳过逻辑:平台无sh时跳过):
| 测试用例 | 验证点 |
|---|---|
test_legacy_attest_writes_root_attestation | 遗留模式将摘要写入根目录.plan-attestation,且与task_plan.md的 SHA-256 完全一致 |
test_show_prints_stored_hash | --show输出中包含完整摘要 |
test_clear_removes_attestation | --clear删除证明文件 |
test_tamper_changes_hash | 篡改计划内容后新摘要与已存摘要必然不同(hook 闸门能触发的必要条件) |
test_parallel_plan_attest_writes_into_plan_dir | 活动计划存在时写入 slug 目录内的.attestation,不写遗留文件 |
test_attest_from_inside_plan_dir_updates_slug_attestation | 从 slug 目录内调用可更新该 slug 的证明,且不创建遗留文件 |
test_failed_explicit_selector_does_not_fallback_inside_slug | PLAN_ID/PWF_PLAN_ROOT指向缺失计划时非零退出,且不产生任何证明文件(不静默证明其他计划) |
test_no_plan_exits_nonzero | 无计划时非零退出 |
test_concurrent_attest_writes_do_not_corrupt_file | 8 个并发 attest 后证明文件始终是完整 64 位十六进制摘要 |
实操总结:完整生命周期
把以上内容串成一份可直接落地的操作流程:
- 创建计划:
/plan生成task_plan.md、findings.md、progress.md; - 人工审阅并批准:确认计划内容符合预期;
- 锁定:执行
/plan-attest(或sh scripts/attest-plan.sh),确认输出的短哈希与存储路径;并行场景下先export PLAN_ID=<slug>或用init-session.sh建号; - 正常推进:每次 UserPromptSubmit / PreToolUse,hook 自动比对摘要;一旦看到
[PLAN TAMPERED — injection blocked],说明计划被非预期改动——先--show查看已存摘要,再决定是git还原还是重新批准; - 有意修改计划:先
/plan-attest --clear解锁 → 编辑 → 重新审阅 → 再次/plan-attest锁定; - v3 模式(autonomous/gated):未证明的计划不会被注入,提示
v3 mode requires attested plan; run attest-plan。
记住信任边界:attestation 防的是"静默篡改",不是"防一切"。把它与PWF_PLAN_ROOT/PLAN_ID的绑定式选择器、slug 并行隔离、原子写入与读回校验组合使用,就是当前仓库给出的最完整、可审计的计划完整性方案。
【免费下载链接】planning-with-filesPersistent file-based planning for AI coding agents and long-running tasks. Crash-proof markdown plans, session recovery after /clear and compaction, per-turn re-injection against context rot, deterministic completion gate. Manus-style. Install from npm, the Claude Code plugin marketplace, or npx skills. Codex, Cursor, OpenCode, 60+ agents.项目地址: https://gitcode.com/GitHub_Trending/pl/planning-with-files
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考