planning-with-files 计划文件锁定:/plan-attest 与 SHA-256 防篡改证明机制深度解析
2026/9/12 16:09:49 网站建设 项目流程

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的标准操作序列(原文档定义):

  1. 解析活动计划:依次优先使用${PLAN_ID}环境变量、.planning/.active_plan文件、最新的.planning/<dir>/目录;若当前目录本身就是合法的.planning/<slug>/目录,则直接使用其中的task_plan.md;否则回退到遗留模式的./task_plan.md
  2. 计算摘要:对解析到的task_plan.md计算 SHA-256。
  3. 写入证明:将十六进制摘要写入.planning/<active-plan>/.attestation(并行计划模式)或./.plan-attestation(遗留模式)。
  4. 向用户确认:打印摘要前 12 位短哈希与存储路径。

关键安全约束:显式指定的${PLAN_ID}${PWF_PLAN_ROOT}若无法解析,命令将以错误退出,绝不回退到其他计划

计划解析:五级优先级与"绑定即不回退"原则

解析逻辑完整实现在 scripts/attest-plan.sh 的resolve_plan_file()(第 44-74 行)中,顺序如下:

  1. ${PLAN_ID}环境变量 →./.planning/$PLAN_ID/
  2. ./.planning/.active_plan指向的计划目录;
  3. 按 mtime 最新的./.planning/<dir>/
  4. 当前目录本身是合法的.planning/<valid-slug>/(由resolve_from_slug_cwd()slug_is_valid()校验,第 25-42 行);
  5. 遗留模式:项目根目录的./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 触发时:

  1. 确定证明文件:遗留模式取${PLAN_PREFIX}.plan-attestation(第 532 行),并行模式取${RESOLVED}/.attestation(第 539 行);
  2. 重算当前摘要:对计划文件实时计算 SHA-256,并去掉 GNU coreutils 可能添加的转义反斜杠前缀(第 1100-1106 行)。实现注释特别强调:mtime 与缓存摘要都不是可信信号,因为文件可以在保持两者不变的情况下被改写,所以每次触发都全量重算
  3. 比对并分支
    • 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.

也就是说,"每次有意编辑并重新批准计划后都要重新运行/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):

  1. 先将摘要写入临时文件attestation_file.tmp.$$
  2. 若有flock,在flock -w 5内执行mv -f(锁文件为证明文件同目录下的.attestation.lock),否则直接原子重命名;
  3. 读回校验:写入完成后读取磁盘上的证明文件,与期望摘要比对(第 233-244 行)。不一致即报错并以非零码退出,杜绝"宣称已锁定、实际未落盘"的陈旧证明被信任;
  4. 失败回退:若首次重命名失败(跨设备、权限等),会通过第二次原子重命名补齐,绝不裸重定向到活文件(那会让并发校验者读到撕裂的中间状态)。

原子重命名是正确性保证——读者永远不会看到半截摘要;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 的autonomousgated模式下是强制的。scripts/inject-plan.sh 中的逻辑(安全项 security-major-4)说明:在无人值守的循环中,计划体会在每一轮注入模型;仅靠 nonce 分隔符无法防御分隔符混淆注入——因为.noncetask_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.sh

slug 模式通过"每会话独立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_slugPLAN_ID/PWF_PLAN_ROOT指向缺失计划时非零退出,且不产生任何证明文件(不静默证明其他计划)
test_no_plan_exits_nonzero无计划时非零退出
test_concurrent_attest_writes_do_not_corrupt_file8 个并发 attest 后证明文件始终是完整 64 位十六进制摘要

实操总结:完整生命周期

把以上内容串成一份可直接落地的操作流程:

  1. 创建计划/plan生成task_plan.mdfindings.mdprogress.md
  2. 人工审阅并批准:确认计划内容符合预期;
  3. 锁定:执行/plan-attest(或sh scripts/attest-plan.sh),确认输出的短哈希与存储路径;并行场景下先export PLAN_ID=<slug>或用init-session.sh建号;
  4. 正常推进:每次 UserPromptSubmit / PreToolUse,hook 自动比对摘要;一旦看到[PLAN TAMPERED — injection blocked],说明计划被非预期改动——先--show查看已存摘要,再决定是git还原还是重新批准;
  5. 有意修改计划:先/plan-attest --clear解锁 → 编辑 → 重新审阅 → 再次/plan-attest锁定;
  6. 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询