Appearance
Claude Agent SDK 的文件检查点功能让你在代理会话中通过 Write、Edit 和 NotebookEdit 工具修改文件后,能够回滚到任意历史状态。你需要启用 enable_file_checkpointing 并设置 extra_args 中的 replay-user-messages 为 null 才能接收到 checkpoint UUID。捕获 checkpoint UUID 后可立即调用 rewindFiles()(TypeScript)或 rewind_files()(Python)恢复文件,或保存 session ID 在流结束后恢复会话再回滚。注意只有上述三个工具跟踪修改,Bash 命令产生的文件变更不会被捕获。
配置 Claude Agent SDK 文件检查点并回滚文件
概述
在代理会话中,当你使用 Write、Edit、NotebookEdit 工具修改文件时,文件检查点会自动备份文件的原始内容。你可以在之后回滚到任何一个检查点,从而撤销不需要的修改、尝试不同的方案或从错误中恢复。
重要限制:仅通过上述三个工具做出的修改会被跟踪。通过 Bash 命令(如 echo > file.txt、sed -i 等)的修改不会被检查点系统记录。
工作原理
启用文件检查点后,SDK 会在每次通过 Write、Edit 或 NotebookEdit 修改文件前创建备份。响应流中的用户消息会包含一个 checkpoint UUID,作为后续的还原点。
支持文件修改的内置工具:
| 工具 | 描述 |
|---|---|
| Write | 创建新文件或覆盖已有文件 |
| Edit | 对已有文件的特定部分进行定向编辑 |
| NotebookEdit | 修改 Jupyter notebook(.ipynb)中的单元格 |
注意:回滚文件只恢复磁盘上的文件状态,并不会回滚对话本身。调用
rewindFiles()(TS)或rewind_files()(Python)后,对话历史与上下文保持不变。
检查点系统跟踪的内容:
- 在会话中创建的文件
- 在会话中修改的文件
- 修改前文件的原始内容
当你回滚到一个检查点时,会话中创建的文件会被删除,修改过的文件会被恢复到对应时刻的内容。
实现检查点
使用文件检查点的完整流程:在选项中启用,从响应流中捕获 checkpoint UUID,然后在需要时调用 rewindFiles()(TS)或 rewind_files()(Python)。
以下示例展示了完整的流程:启用检查点、从响应流中捕获 checkpoint UUID 和 session ID,之后恢复会话并回滚文件。
python
import asyncio
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
UserMessage,
ResultMessage,
)
async def main():
# Step 1: 启用检查点
options = ClaudeAgentOptions(
enable_file_checkpointing=True,
permission_mode="acceptEdits", # 自动接受文件编辑,无需提示
extra_args={
"replay-user-messages": None
}, # 必须设置才能接收 checkpoint UUID
)
checkpoint_id = None
session_id = None
# 执行查询并捕获 checkpoint UUID 和 session ID
async with ClaudeSDKClient(options) as client:
await client.query("Refactor the authentication module")
# Step 2: 从第一条用户消息中捕获 checkpoint UUID
async for message in client.receive_response():
if isinstance(message, UserMessage) and message.uuid and not checkpoint_id:
checkpoint_id = message.uuid
if isinstance(message, ResultMessage) and not session_id:
session_id = message.session_id
# Step 3: 稍后,通过空提示恢复会话并回滚
if checkpoint_id and session_id:
async with ClaudeSDKClient(
ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
) as client:
await client.query("") # 空提示打开连接
async for message in client.receive_response():
await client.rewind_files(checkpoint_id)
break
print(f"Rewound to checkpoint: {checkpoint_id}")
asyncio.run(main())typescript
import { query } from "@anthropic-ai/claude-agent-sdk";
async function main() {
// Step 1: 启用检查点
const opts = {
enableFileCheckpointing: true,
permissionMode: "acceptEdits" as const, // 自动接受文件编辑
extraArgs: { "replay-user-messages": null } // 必须设置才能接收 checkpoint UUID
};
const response = query({
prompt: "Refactor the authentication module",
options: opts
});
let checkpointId: string | undefined;
let sessionId: string | undefined;
// Step 2: 从第一条用户消息中捕获 checkpoint UUID
for await (const message of response) {
if (message.type === "user" && message.uuid && !checkpointId) {
checkpointId = message.uuid;
}
if ("session_id" in message && !sessionId) {
sessionId = message.session_id;
}
}
// Step 3: 稍后,通过空提示恢复会话并回滚
if (checkpointId && sessionId) {
const rewindQuery = query({
prompt: "", // 空提示打开连接
options: { ...opts, resume: sessionId }
});
for await (const msg of rewindQuery) {
await rewindQuery.rewindFiles(checkpointId);
break;
}
console.log(`Rewound to checkpoint: ${checkpointId}`);
}
}
main();启用检查点并接收 checkpoint UUID
在 SDK 选项中配置以下参数:
| 选项 | Python | TypeScript | 描述 |
|---|---|---|---|
| 启用检查点 | enable_file_checkpointing=True | enableFileCheckpointing: true | 跟踪文件修改以支持回滚 |
| 接收 checkpoint UUID | extra_args={"replay-user-messages": None} | extraArgs: { 'replay-user-messages': null } | 在流中获取用户消息 UUID |
python
options = ClaudeAgentOptions(
enable_file_checkpointing=True,
permission_mode="acceptEdits",
extra_args={"replay-user-messages": None},
)
async with ClaudeSDKClient(options) as client:
await client.query("Refactor the authentication module")typescript
const response = query({
prompt: "Refactor the authentication module",
options: {
enableFileCheckpointing: true,
permissionMode: "acceptEdits" as const,
extraArgs: { "replay-user-messages": null }
}
});捕获 checkpoint UUID 和 session ID
设置 replay-user-messages 后,每个用户消息都会携带一个 UUID,你可以用它作为检查点。通常捕获第一个用户消息的 UUID 即可:回滚到它会使所有文件回到原始状态。如果你需要多个还原点,参见多还原点。
捕获 session ID 是可选的;只有在流结束后需要回滚时才需要。如果你在流尚未结束时立即调用 rewindFiles()(如危险操作前备份模式),则无需捕获 session ID。
python
checkpoint_id = None
session_id = None
async for message in client.receive_response():
# 每次遇到用户消息都更新 checkpoint(保留最新)
if isinstance(message, UserMessage) and message.uuid:
checkpoint_id = message.uuid
# 从结果消息中捕获 session ID
if isinstance(message, ResultMessage):
session_id = message.session_idtypescript
let checkpointId: string | undefined;
let sessionId: string | undefined;
for await (const message of response) {
// 每次更新最新 checkpoint
if (message.type === "user" && message.uuid) {
checkpointId = message.uuid;
}
// 从任意消息中获取 session ID
if ("session_id" in message) {
sessionId = message.session_id;
}
}流结束后回滚
要在流结束后回滚,先用空提示恢复会话,然后在新的响应中调用 rewind_files() 或 rewindFiles()。你也可以在流处理过程中回滚,见危险操作前备份。
python
async with ClaudeSDKClient(
ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
) as client:
await client.query("") # 空提示打开连接
async for message in client.receive_response():
await client.rewind_files(checkpoint_id)
breaktypescript
const rewindQuery = query({
prompt: "", // 空提示打开连接
options: { ...opts, resume: sessionId }
});
for await (const msg of rewindQuery) {
await rewindQuery.rewindFiles(checkpointId);
break;
}你也可以通过 CLI 回滚:
bash
claude -p --resume <session-id> --rewind-files <checkpoint-uuid>常见模式
根据你的使用场景,捕获和使用 checkpoint UUID 的方式不同。
危险操作前备份
此模式只保留最新的 checkpoint UUID,在每一轮代理执行前更新。如果在处理过程中出现问题,可以立即回滚到最后一个安全状态并退出循环。
python
import asyncio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, UserMessage
async def main():
options = ClaudeAgentOptions(
enable_file_checkpointing=True,
permission_mode="acceptEdits",
extra_args={"replay-user-messages": None},
)
safe_checkpoint = None
async with ClaudeSDKClient(options) as client:
await client.query("Refactor the authentication module")
async for message in client.receive_response():
# 每轮开始前更新 checkpoint(覆盖之前的)
if isinstance(message, UserMessage) and message.uuid:
safe_checkpoint = message.uuid
# 根据你的逻辑决定何时回滚
# 例如:错误检测、验证失败或用户输入
if your_revert_condition and safe_checkpoint:
await client.rewind_files(safe_checkpoint)
# 回滚后退出循环,文件已恢复
break
asyncio.run(main())typescript
import { query } from "@anthropic-ai/claude-agent-sdk";
async function main() {
const response = query({
prompt: "Refactor the authentication module",
options: {
enableFileCheckpointing: true,
permissionMode: "acceptEdits" as const,
extraArgs: { "replay-user-messages": null }
}
});
let safeCheckpoint: string | undefined;
for await (const message of response) {
// 每轮开始前更新 checkpoint(覆盖之前的)
if (message.type === "user" && message.uuid) {
safeCheckpoint = message.uuid;
}
// 根据你的逻辑决定何时回滚
if (yourRevertCondition && safeCheckpoint) {
await response.rewindFiles(safeCheckpoint);
// 回滚后退出循环,文件已恢复
break;
}
}
}
main();多还原点
如果 Claude 在多个轮次中做了多个修改,你可能只想回滚到某个特定点,而不是全部回退。例如 Claude 在第一轮重构文件,第二轮添加测试,你可以保留重构但撤销测试。
此模式将所有 checkpoint UUID 存储在数组中并附带元数据。会话结束后,可以恢复到任意历史检查点:
python
import asyncio
from dataclasses import dataclass
from datetime import datetime
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
UserMessage,
ResultMessage,
)
@dataclass
class Checkpoint:
id: str
description: str
timestamp: datetime
async def main():
options = ClaudeAgentOptions(
enable_file_checkpointing=True,
permission_mode="acceptEdits",
extra_args={"replay-user-messages": None},
)
checkpoints = []
session_id = None
async with ClaudeSDKClient(options) as client:
await client.query("Refactor the authentication module")
async for message in client.receive_response():
if isinstance(message, UserMessage) and message.uuid:
checkpoints.append(
Checkpoint(
id=message.uuid,
description=f"After turn {len(checkpoints) + 1}",
timestamp=datetime.now(),
)
)
if isinstance(message, ResultMessage) and not session_id:
session_id = message.session_id
# 稍后:恢复会话并回滚到任意检查点
if checkpoints and session_id:
target = checkpoints[0] # 选择任意检查点
async with ClaudeSDKClient(
ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
) as client:
await client.query("") # 空提示打开连接
async for message in client.receive_response():
await client.rewind_files(target.id)
break
print(f"Rewound to: {target.description}")
asyncio.run(main())typescript
import { query } from "@anthropic-ai/claude-agent-sdk";
interface Checkpoint {
id: string;
description: string;
timestamp: Date;
}
async function main() {
const opts = {
enableFileCheckpointing: true,
permissionMode: "acceptEdits" as const,
extraArgs: { "replay-user-messages": null }
};
const response = query({
prompt: "Refactor the authentication module",
options: opts
});
const checkpoints: Checkpoint[] = [];
let sessionId: string | undefined;
for await (const message of response) {
if (message.type === "user" && message.uuid) {
checkpoints.push({
id: message.uuid,
description: `After turn ${checkpoints.length + 1}`,
timestamp: new Date()
});
}
if ("session_id" in message && !sessionId) {
sessionId = message.session_id;
}
}
// 稍后:恢复会话并回滚到任意检查点
if (checkpoints.length > 0 && sessionId) {
const target = checkpoints[0]; // 选择任意检查点
const rewindQuery = query({
prompt: "", // 空提示打开连接
options: { ...opts, resume: sessionId }
});
for await (const msg of rewindQuery) {
await rewindQuery.rewindFiles(target.id);
break;
}
console.log(`Rewound to: ${target.description}`);
}
}
main();Try it out
这个完整示例创建了一个小工具文件,让 agent 添加文档注释,展示改动,然后询问你是否要回滚。
开始前,请确保已安装 Claude Agent SDK。
创建工具文件
创建 utils.py(Python)或 utils.ts(TypeScript),粘贴以下代码:
python
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / btypescript
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
export function divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}创建检查点脚
在相同目录下创建 try_checkpointing.py 或 try_checkpointing.ts,粘贴以下代码。该脚本请求 Claude 为工具文件添加注释,然后询问你是否要回滚还原。
python
import asyncio
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
UserMessage,
ResultMessage,
)
async def main():
options = ClaudeAgentOptions(
enable_file_checkpointing=True,
permission_mode="acceptEdits",
extra_args={"replay-user-messages": None},
)
checkpoint_id = None
session_id = None
print("Running agent to add doc comments to utils.py...\n")
async with ClaudeSDKClient(options) as client:
await client.query("Add doc comments to utils.py")
async for message in client.receive_response():
if isinstance(message, UserMessage) and message.uuid and not checkpoint_id:
checkpoint_id = message.uuid
if isinstance(message, ResultMessage):
session_id = message.session_id
print("Done! Open utils.py to see the added doc comments.\n")
if checkpoint_id and session_id:
response = input("Rewind to remove the doc comments? (y/n): ")
if response.lower() == "y":
async with ClaudeSDKClient(
ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
) as client:
await client.query("")
async for message in client.receive_response():
await client.rewind_files(checkpoint_id)
break
print("\n✓ File restored! Open utils.py to verify the doc comments are gone.")
else:
print("\nKept the modified file.")
asyncio.run(main())typescript
import { query } from "@anthropic-ai/claude-agent-sdk";
import * as readline from "readline";
async function main() {
const opts = {
enableFileCheckpointing: true,
permissionMode: "acceptEdits" as const,
extraArgs: { "replay-user-messages": null }
};
let sessionId: string | undefined;
let checkpointId: string | undefined;
console.log("Running agent to add doc comments to utils.ts...\n");
const response = query({
prompt: "Add doc comments to utils.ts",
options: opts
});
for await (const message of response) {
if (message.type === "user" && message.uuid && !checkpointId) {
checkpointId = message.uuid;
}
if ("session_id" in message) {
sessionId = message.session_id;
}
}
console.log("Done! Open utils.ts to see the added doc comments.\n");
if (checkpointId && sessionId) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const answer = await new Promise<string>((resolve) => {
rl.question("Rewind to remove the doc comments? (y/n): ", resolve);
});
rl.close();
if (answer.toLowerCase() === "y") {
const rewindQuery = query({
prompt: "",
options: { ...opts, resume: sessionId }
});
for await (const msg of rewindQuery) {
await rewindQuery.rewindFiles(checkpointId);
break;
}
console.log("\n✓ File restored! Open utils.ts to verify the doc comments are gone.");
} else {
console.log("\nKept the modified file.");
}
}
}
main();这个示例演示了完整的检查点工作流:
- 启用检查点:配置 SDK 时设置
enable_file_checkpointing=True和permission_mode="acceptEdits"以自动授权文件编辑 - 捕获检查点数据:在 agent 运行过程中存储第一个用户消息 UUID(还原点)和 session ID
- 询问回滚:agent 结束后检查工具文件的注释,决定是否撤销更改
- 恢复并回滚:选择“是”后,用空提示恢复会话并调用
rewind_files()还原原始文件
运行脚本
在工具文件所在目录运行脚本:
bash
# Python
python try_checkpointing.pybash
# TypeScript
npx tsx try_checkpointing.ts在运行前,最好在 IDE 中打开工具文件,你能实时看到 agent 添加注释再被撤销的变化。
限制
| 限制 | 描述 |
|---|---|
| 仅 Write/Edit/NotebookEdit 工具 | Bash 命令做的修改不被跟踪 |
| 绑定到同一个会话 | 检查点仅与创建它的会话关联 |
| 仅文件内容 | 创建、移动或删除目录的操作不会被回滚撤销 |
| 本地文件 | 远程或网络文件不纳入跟踪 |
排查
enableFileCheckpointing 或 rewindFiles() 不可用
原因:SDK 版本太旧。
解决方案:升级到最新版本:
- Python:
pip install --upgrade claude-agent-sdk - TypeScript:
npm install @anthropic-ai/claude-agent-sdk@latest
用户消息没有 UUID
message.uuid 为 undefined 或缺失。
原因:未设置 replay-user-messages 选项。
解决方案:在选项中添加 extra_args={"replay-user-messages": None}(Python)或 extraArgs: { 'replay-user-messages': null }(TypeScript)。
"No file checkpoint found for message" 错误
指定用户消息 UUID 对应的检查点数据不存在。
常见原因:
- 原始会话未启用文件检查点(
enable_file_checkpointing或enableFileCheckpointing未设为true) - 尝试恢复并回滚前,会话未正确结束
解决方案:确保原始会话已设置 enable_file_checkpointing=True(Python)或 enableFileCheckpointing: true(TypeScript),然后按示例模式:捕获第一条用户消息 UUID,完全结束会话,再用空提示恢复并调用一次 rewindFiles()。
"ProcessTransport is not ready for writing" 错误
当你完成响应流迭代后,再调用 rewindFiles() 或 rewind_files() 时会出现此错误。因为循环结束后与 CLI 进程的连接已关闭。
解决方案:用空提示恢复会话,然后在新查询中调用回滚:
python
async with ClaudeSDKClient(
ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
) as client:
await client.query("")
async for message in client.receive_response():
await client.rewind_files(checkpoint_id)
breaktypescript
const rewindQuery = query({
prompt: "",
options: { ...opts, resume: sessionId }
});
for await (const msg of rewindQuery) {
await rewindQuery.rewindFiles(checkpointId);
break;
}下一步
- [会话管理](/en/agent-sdk/sessions):学习如何恢复会话,这是流结束后回滚的前提。涵盖 session ID、对话恢复和会话分支。
- [权限配置](/en/agent-sdk/permissions):配置 Claude 可以使用哪些工具以及如何批准文件修改。当需要对编辑时机进行更细粒度的控制时尤其有用。
- [TypeScript SDK 参考](/en/agent-sdk/typescript):完整的 API 参考,包括
query()的所有选项和rewindFiles()方法。 - [Python SDK 参考](/en/agent-sdk/python):完整的 API 参考,包括
ClaudeAgentOptions的所有选项和rewind_files()方法。
常见问题
检查点可以回滚到初始状态吗?
可以。捕获对话中第一个用户消息的 UUID 作为检查点,回滚到它会使所有文件恢复到初始状态。这是最常用的还原策略。如果你需要保留某些中间修改,可以存储多个 checkpoint UUID。
我用 Bash 命令修改了文件,为什么回滚无效?
文件检查点只跟踪 Write、Edit 和 NotebookEdit 工具所做的修改。通过 Bash 命令(如 echo、sed、mv 等)直接修改文件不会被记录,因此无法通过检查点回滚。解决方法是在脚本中始终使用 SDK 的工具来修改文件。
回滚后文件没有恢复到之前的状态怎么办?
首先确认你使用了正确的 checkpoint UUID。然后验证原始会话已启用 enable_file_checkpointing。如果错误是“No file checkpoint found for message”,请检查是否在流结束后才调用回滚,确保先恢复会话再调用 rewindFiles()。