用 Claude Code 安全自动化重构的实战流程
用 Claude Code 做安全 refactoring:前后对比、测试、git diff、评审清单、常见风险和可复制 prompt。
先定义安全边界,而不是直接让它改全部代码
用 Claude Code 自动化 refactoring 时,最危险的开场不是复杂任务,而是过于简单的一句话:把这个项目整理一下。这个请求看起来高效,但通常会得到一个很大的 diff,里面混合命名、拆函数、格式化、错误处理甚至行为变化。重构的定义是:改善内部结构,但不改变用户、API、测试和下游系统依赖的外部行为。
这篇文章把 Claude Code 当成实务伙伴,而不是魔法按钮:先调查,再给小计划,只改一个主题,运行验证命令,最后解释 git diff。官方的 common workflows 很适合做参考。命令权限和项目配置可以看 settings。
如果团队还在建立使用规则,可以先阅读 Claude Code 权限设置 和 Claude Code 上下文管理。本文更关注每天怎么做:先问什么,哪些任务适合初学者,怎样测试,怎样通过 diff 评审。
Masa 的验证笔记:小规模试用时,Claude Code 在变量重命名、抽取纯函数、补 TypeScript 类型、增加回归测试方面表现稳定。反过来,像“把这个 service 现代化”这种宽泛 prompt,很容易让 diff 变大。真正能长期使用的 workflow 往往很朴素:范围小、测试明确、diff 可读。
安全 workflow:调查、计划、一个 diff、验证
在团队形成信任之前,固定使用这个顺序。
| 步骤 | Claude Code 做什么 | 人要检查什么 |
|---|---|---|
| 1. 调查 | 阅读目标文件、依赖和测试 | 范围是否过大 |
| 2. 计划 | 给出不超过三步的小计划 | 是否隐藏行为变化 |
| 3. 编辑 | 只改一个主题 | diff 是否能读完 |
| 4. 验证 | 运行 test、typecheck、lint | 失败是否解释清楚 |
| 5. 评审 | 总结 git diff 和风险 | before/after 行为是否一致 |
第一条 prompt 不允许编辑。
请检查这个仓库,找出适合安全 refactoring 的候选项。
现在不要编辑任何文件。
条件:
- 不改变外部行为
- 每个 diff 最多 3 个文件
- 优先选择已有测试覆盖的区域
- 用表格输出候选项、原因、验证命令、风险
“现在不要编辑任何文件”很关键。Claude Code 读完代码后可能直接行动。把调查和实现分开,可以显著降低事故概率。
开始前先建分支并记录 baseline。
git status --short
git checkout -b refactor/safe-extract-order-total
npm test
npm run typecheck
npm run lint
项目命令可能不同,请以 package.json 为准。如果测试在修改前已经失败,要先记录。否则后面无法判断失败是原本存在,还是重构引入。
Use case 1: 从重命名和抽取纯函数开始
最安全的第一步是改善命名和抽取纯函数。纯函数指同样输入总是得到同样输出,并且不更新数据库、不发邮件、不调用外部 API、不修改全局状态。Claude Code 在这类任务上很适合,因为成功条件容易测试。
// before: src/domain/order.ts
export function calc(o: { items: { p: number; q: number }[]; d?: number }) {
let t = 0;
for (const i of o.items) {
t += i.p * i.q;
}
if (o.d) {
t = t - o.d;
}
return Math.max(t, 0);
}
这段代码很短,但 p、q、d 没有表达业务含义。让 Claude Code 先补测试,再改善名称。
请安全重构 src/domain/order.ts 中的 calc 函数。
要求:
- 先添加单元测试,固定当前行为,再修改实现
- 本次 diff 保留导出的函数名 calc
- 改善变量名和类型名
- 保留 total 不会小于 0 的规则
- 修改后运行 npm test -- order
理想的 after 应该很小。
// after: src/domain/order.ts
type OrderLine = {
price: number;
quantity: number;
};
type OrderInput = {
items: OrderLine[];
discount?: number;
};
export function calc(order: OrderInput): number {
const subtotal = order.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return Math.max(subtotal - (order.discount ?? 0), 0);
}
可复制的测试:
// src/domain/order.test.ts
import { describe, expect, it } from "vitest";
import { calc } from "./order";
describe("calc", () => {
it("multiplies price and quantity", () => {
expect(calc({ items: [{ price: 1200, quantity: 2 }] })).toBe(2400);
});
it("applies discount without returning a negative total", () => {
expect(calc({ items: [{ price: 500, quantity: 1 }], discount: 800 })).toBe(0);
});
});
评审只看被改动的文件。
git diff -- src/domain/order.ts src/domain/order.test.ts
npm test -- order
npm run typecheck
这里的评审问题不是“代码看起来是否聪明”,而是“相同输入是否仍然代表相同业务含义”。检查计算结果、导出函数名和测试描述。
Use case 2: 删除 any 时先固定边界类型
减少 any 很有价值,但一次性全项目清理通常是 mistake。应该从边界开始:API 响应、表单输入、配置文件、webhook、导入数据。这些地方是未知数据进入系统的入口。
// before: src/lib/user-api.ts
export async function fetchUser(id: string): Promise<any> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
export function getDisplayName(user: any): string {
return user.profile.displayName || user.name;
}
prompt 要给出窄目标,并说明缺失数据的处理方式。
请减少 src/lib/user-api.ts 中的 any。
要求:
- 为 API 响应添加类型
- 不改变 fetch URL 和返回值含义
- profile 缺失时 getDisplayName 不能崩溃
- 为当前 display name 行为添加测试
- 运行 npm test -- user-api 和 npm run typecheck
一个可接受的第一版 diff:
// after: src/lib/user-api.ts
export type UserResponse = {
id: string;
name: string;
profile?: {
displayName?: string;
};
};
export async function fetchUser(id: string): Promise<UserResponse> {
const response = await fetch(`/api/users/${id}`);
return response.json() as Promise<UserResponse>;
}
export function getDisplayName(user: UserResponse): string {
return user.profile?.displayName ?? user.name;
}
注意,这个 cast 并不会验证运行时数据。如果项目需要真正的输入安全,可以在第二个 diff 中加入 zod 或项目已有 parser。不要把“删除 any”和“引入验证库”混在第一个初学者 diff 里。
// src/lib/user-api.test.ts
import { describe, expect, it } from "vitest";
import { getDisplayName, type UserResponse } from "./user-api";
describe("getDisplayName", () => {
it("uses profile displayName when present", () => {
const user: UserResponse = {
id: "u1",
name: "Masa",
profile: { displayName: "Masa I." },
};
expect(getDisplayName(user)).toBe("Masa I.");
});
it("falls back to name when profile is missing", () => {
expect(getDisplayName({ id: "u2", name: "Guest" })).toBe("Guest");
});
});
评审时要找危险捷径:as any、吞掉错误、用空字符串当 fallback、改变 optional field 的语义。类型更安全的 diff 仍然可能破坏行为。
Use case 3: 大函数先建立 test harness 再拆
大 service 函数很适合优化,但风险也高。订单、计费、权限、通知、导入任务常常混合验证、计算、持久化和副作用。让 Claude Code 先抽取一个纯计算片段,不要一次改完整流程。
// before: src/services/order-service.ts
export async function createOrder(input: CreateOrderInput) {
if (input.items.length === 0) {
throw new Error("items required");
}
const subtotal = input.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const shippingFee = subtotal >= 10000 ? 0 : 800;
const total = subtotal + shippingFee;
const order = await db.order.create({
data: { userId: input.userId, subtotal, shippingFee, total },
});
await mailer.sendOrderCreated(order.id);
return order;
}
prompt 要明确排除项。
请让 src/services/order-service.ts 中的 createOrder 变小。
本次 diff 只做:
- 把运费和总额计算抽成纯函数
- 函数名为 calculateOrderTotals
- 为 calculateOrderTotals 添加单元测试
- 保持数据库写入和邮件发送顺序不变
本次 diff 不做:
- 修改数据库 schema
- 修改错误文案
- 修改 API 响应结构
- 移动无关函数
- 格式化整个文件
after 可以保持朴素。
// after: src/services/order-service.ts
export function calculateOrderTotals(items: OrderItem[]) {
const subtotal = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const shippingFee = subtotal >= 10000 ? 0 : 800;
return {
subtotal,
shippingFee,
total: subtotal + shippingFee,
};
}
评审命令:
git diff --stat
git diff -- src/services/order-service.ts
git diff -- src/services/order-service.test.ts
npm test -- order-service
如果 Claude Code 顺手格式化了无关代码,就缩小 diff。
这个 diff 太大。
请撤回纯格式化修改,只保留 calculateOrderTotals 的抽取和测试。
不要改变外部行为、错误文案、数据库写入、邮件发送顺序。
用 git diff 评审,不要凭感觉
Claude Code 的说明有帮助,但 diff 才是事实。
git diff --check
git diff --stat
git diff --name-only
git diff --word-diff -- src/domain/order.ts
| 评审点 | 需要检查 |
|---|---|
| 行为 | 输入、输出、异常、HTTP status、持久化顺序是否不变 |
| diff 大小 | 是否能在一次人工 review 中读完 |
| 测试 | 原有行为是否被新旧测试保护 |
| 类型 | 是否新增 as any、危险 cast、忽略错误 |
| 副作用 | API、邮件、计费、删除、权限顺序是否不变 |
| 摘要 | Claude Code 的总结是否与实际 diff 一致 |
可复用 review prompt:
请 review 这个 git diff。
检查:
- 是否超出 refactoring 范围
- 哪些行为没有测试保护
- 是否有危险 cast 或吞掉错误
- 哪些文件需要人工重点检查
按三类输出:
- 看起来安全
- 需要人工确认
- 必须修改
并给出文件名和原因。
Pitfall: 常见 failure 和 risk
第一个 failure 是 prompt 太宽。
把这个 service layer 变干净。
这会把抽函数、命名、错误设计、文件移动、格式化混在一起。更安全的写法是:
只把 createOrder 中的运费计算抽成纯函数。
不要改变处理顺序、错误文案、返回值。
第二个 risk 是没有测试就接受“看起来很干净”的 diff。可读性变好,不代表边界条件没变。折扣、免运费、权限拒绝、重试逻辑、null 处理都要先测试。第三个 mistake 是把格式化和结构重构放在一起。Prettier 改了几百行后,真正的行为变化会被隐藏。第四个 risk 是一开始就开放过多命令权限。先从读取、test、typecheck、lint 开始,稳定后再扩大。
适合初学者的推荐顺序
如果你第一次把这套方法放进真实项目,不要从最复杂的 service 开始。推荐顺序是:第一天只做变量名和类型名整理,第二天做纯函数抽取,第三天减少一个边界文件里的 any,第四天再尝试拆分大函数。每次都保留独立 branch,并且只在测试通过、diff 可读、review checklist 全部满足时合并。
团队使用时,还可以把成功案例写进 CLAUDE.md。例如“订单金额计算只能先加测试再改实现”“邮件发送顺序不能改变”“权限判定相关代码必须人工二次确认”。这些规则越具体,Claude Code 越容易沿着安全路线工作。相反,如果只写“保持代码质量”,模型不知道哪些风险最重要。
更实际的做法是为每类 refactoring 准备模板 prompt。命名改善、类型收紧、函数抽取、测试补强分别用不同模板。这样新人也不会一上来让 Claude Code 大范围重写。自动化的目标不是让人完全不看代码,而是把重复劳动缩小到可验证、可复用、可审查的步骤里。
Checklist 与 CTA
## Refactoring checklist
- [ ] 本次修改只有一个目的
- [ ] 编辑前已运行 baseline tests
- [ ] before/after 行为等价
- [ ] 新测试或旧测试保护了行为
- [ ] git diff --stat 足够小
- [ ] git diff --check 通过
- [ ] 没有新增 any、危险 cast、吞掉错误
- [ ] DB、邮件、计费、删除、权限顺序不变
最终 prompt:
执行一个安全 refactoring diff。
目标:
- src/services/order-service.ts
- src/services/order-service.test.ts
成功条件:
- 外部行为不变
- 抽取 calculateOrderTotals
- 旧测试和新测试通过
- 报告 git diff --stat 和执行过的命令
禁止:
- DB schema 修改
- API response 修改
- 错误文案修改
- 编辑无关文件
我的验证结果是,最有效的两个习惯是:先要求“只计划不编辑”,以及每次实现后强制输出 git diff 总结。你也可以把这个 workflow 和 Claude Code review checklist 以及 CLAUDE.md best practices 结合起来,让团队重复使用。
如果团队想把 Claude Code 安全落地,Claude Code training 可以一起设计权限、prompt、review 规则和 workflow。重构自动化真正有价值的时候,不是 diff 看起来很炫,而是维护风险稳定下降。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。