能撤销、又不丢会话的清洗

一个没法撤销的清洗,人在按回车之前总会犹豫。所以 spillscan 加了 spillscan undo。难点不在于把 .bak 拷回去,而在于 agent 的会话记录会一直增长:你继续干活时,Claude Code 和 Codex 会往同一个 session JSONL 里追加内容。

天真的撤销会直接用备份覆盖文件,悄悄删掉清洗之后写进来的一切。spillscan 更严格:对每个 <file>.bak,它对备份重新跑一遍同样的确定性清洗,然后检查当前文件是否仍然以这些字节开头。于是只有三种情况:

$ spillscan undo
UNDO (dry-run)  backups=2 restorable=1
STATE     FILE                             NOTE
appended  ~/.claude/projects/…/a.jsonl     keeps 1 line the agent wrote after the scrub
diverged  ~/.claude/projects/…/b.jsonl     edited since the scrub; compare with …/b.jsonl.bak by hand
Would restore 1 transcript, secrets included. Nothing changed. Apply with: spillscan undo --write

和 redact 一样,不加 --write 就只是预演。恢复时先写临时文件再 rename,原文落盘之后才删除 .bak。

做这个功能时还顺手揪出一个更隐蔽的 bug:先跑一次 redact --write,之后 agent 又在同一个会话里打印了一个新密钥,你再清洗一次,第二次会用已经清洗过的文件覆盖 .bak,第一次的原文就没了。现在重复清洗只会把新增的尾部追加进已有备份;如果那里放着一个不是 spillscan 写的 .bak,它会直接拒绝并说明原因。

有一点没变:undo 会把明文密钥放回磁盘。无论撤不撤销,都请先去服务商那边轮换密钥。

该功能已在 main 上,会随下一个版本发布。源码与 README。

— 2026-10-21