能撤销、又不丢会话的清洗
一个没法撤销的清洗,人在按回车之前总会犹豫。所以 spillscan 加了 spillscan undo。难点不在于把 .bak 拷回去,而在于 agent 的会话记录会一直增长:你继续干活时,Claude Code 和 Codex 会往同一个 session JSONL 里追加内容。
天真的撤销会直接用备份覆盖文件,悄悄删掉清洗之后写进来的一切。spillscan 更严格:对每个 <file>.bak,它对备份重新跑一遍同样的确定性清洗,然后检查当前文件是否仍然以这些字节开头。于是只有三种情况:
intact:清洗后没变过,原文逐字节恢复,CRLF 和末尾缺失的换行都保持原样。appended:agent 还在写。原文恢复,新追加的行原封不动接在后面。diverged:文件被别的方式改过。spillscan 不动它,也保留它的.bak,提示你手动对比,并以退出码 1 结束。
$ 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