Skip to content

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.txtsed -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 选项中配置以下参数:

选项PythonTypeScript描述
启用检查点enable_file_checkpointing=TrueenableFileCheckpointing: true跟踪文件修改以支持回滚
接收 checkpoint UUIDextra_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_id
typescript
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)
        break
typescript
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 / b
typescript
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.pytry_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();

这个示例演示了完整的检查点工作流:

  1. 启用检查点:配置 SDK 时设置 enable_file_checkpointing=Truepermission_mode="acceptEdits" 以自动授权文件编辑
  2. 捕获检查点数据:在 agent 运行过程中存储第一个用户消息 UUID(还原点)和 session ID
  3. 询问回滚:agent 结束后检查工具文件的注释,决定是否撤销更改
  4. 恢复并回滚:选择“是”后,用空提示恢复会话并调用 rewind_files() 还原原始文件

运行脚本

在工具文件所在目录运行脚本:

bash
# Python
python try_checkpointing.py
bash
# 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.uuidundefined 或缺失。

原因:未设置 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_checkpointingenableFileCheckpointing 未设为 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)
        break
typescript
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 命令(如 echosedmv 等)直接修改文件不会被记录,因此无法通过检查点回滚。解决方法是在脚本中始终使用 SDK 的工具来修改文件。

回滚后文件没有恢复到之前的状态怎么办?

首先确认你使用了正确的 checkpoint UUID。然后验证原始会话已启用 enable_file_checkpointing。如果错误是“No file checkpoint found for message”,请检查是否在流结束后才调用回滚,确保先恢复会话再调用 rewindFiles()