一、概述
BeforeWrite 和 AfterWrite 是 Claude Code 钩子(Hook)系统中的一对关键生命周期钩子,分别在文件写入前和文件写入后触发。它们允许开发者插入自定义逻辑,实现对文件操作的精细化控制,是构建安全、高效、自动化开发工作流的核心工具。
核心概念:
- BeforeWrite: 在 Claude Code 将内容写入文件之前触发,可对即将写入的内容进行验证、转换或备份
- AfterWrite: 在文件成功写入磁盘之后触发,可用于格式检查、同步触发或后续处理
- 两者共同构成文件写入生命周期的完整控制闭环
- 通过
settings.json 中的 hooks.BeforeWrite 和 hooks.AfterWrite 配置
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ ┌─────────────┐
│ Claude Code │────▶│ BeforeWrite 钩子 │────▶│ 写入文件磁盘 │────▶│ AfterWrite 钩子 │
│ 生成内容 │ │ (验证/备份/转换) │ │ │ │(检查/同步/通知)│
└─────────────┘ └──────────────────┘ └──────────────┘ └─────────────┘
提示
BeforeWrite 和 AfterWrite 钩子是 Claude Code 自动化能力的重要组成部分。合理使用这对钩子,可以有效防止错误写入、保障文件安全、维持代码质量,并触发下游自动化流程。
二、BeforeWrite 钩子详解
BeforeWrite 钩子在 Claude Code 即将将内容写入文件系统时触发,此时文件尚未被修改。钩子脚本接收到待写入的文件路径和内容,可以执行验证、备份或内容转换操作。
触发时机
- Claude Code 调用 Write 工具创建新文件时
- Claude Code 调用 Edit 工具修改现有文件时(本质也是写入操作)
- 任何通过 Claude Code 内部机制产生文件写入的操作
BeforeWrite 的核心职责
- 内容验证(Validation): 检查待写入内容是否符合项目规范,如文件大小限制、编码格式、敏感信息检测等
- 自动备份(Backup): 在覆盖现有文件前自动创建备份副本,防止误操作导致内容丢失
- 内容转换(Transformation): 对生成的内容进行自动修正或格式化,如代码风格统一、占位符替换等
- 权限检查(Permission): 验证文件写入路径是否在允许范围内,阻止越权写入
钩子脚本接收的上下文
| 参数 |
说明 |
类型 |
filePath |
即将写入的目标文件完整路径 |
字符串 |
content |
即将写入的文件内容 |
字符串 |
hookType |
钩子类型标识,固定为 "BeforeWrite" |
字符串 |
BeforeWrite 返回值说明
- 返回修改后的内容: 对原始内容进行转换后返回新的内容字符串,Claude Code 将写入返回的内容
- 返回原始内容: 不做修改,直接透传,Claude Code 按原始内容写入
- 抛出错误: 阻止写入操作,Claude Code 收到错误后将放弃本次写入并向用户报告
三、AfterWrite 钩子详解
AfterWrite 钩子在文件成功写入磁盘后触发。此时文件已经写入完成,钩子可以读取已写入的文件内容执行后续处理,但无法再阻止写入发生。
触发时机
- 文件写入操作成功完成后立即触发
- 在 Claude Code 继续后续操作之前执行
- 如果 BeforeWrite 钩子阻止了写入,AfterWrite 不会被触发
AfterWrite 的核心职责
- 格式检查(Format Check): 对已写入的文件执行代码格式化或 lint 检查,确保代码风格一致
- 同步触发(Sync Trigger): 触发文件同步、部署更新或 CI/CD 流程
- 日志记录(Logging): 记录文件变更日志,追踪修改历史
- 通知发送(Notification): 向外部系统发送变更通知,如 Webhook 回调
钩子脚本接收的上下文
| 参数 |
说明 |
类型 |
filePath |
已写入的目标文件完整路径 |
字符串 |
hookType |
钩子类型标识,固定为 "AfterWrite" |
字符串 |
与 BeforeWrite 的关键区别
AfterWrite 无法接收写入时的 content 参数(文件已写入磁盘),也不能阻止写入结果。它的设计定位是"后处理"而非"预检"。如果需要阻止写入,必须使用 BeforeWrite。
AfterWrite 返回值说明
- 通常返回成功状态即可
- 如果钩子执行失败(如格式化出错),可以向用户报告错误但文件已写入完成
- 建议在 AfterWrite 中对写入后的文件再次读取并执行检查
四、配置方法
BeforeWrite 和 AfterWrite 钩子通过 Claude Code 的 settings.json 文件进行配置。配置文件位于项目根目录的 .claude/settings.json 或全局用户设置目录。
基本配置结构
在 settings.json 的 hooks 字段中分别配置 BeforeWrite 和 AfterWrite:
{
"hooks": {
"BeforeWrite": "node .claude/hooks/before-write.js",
"AfterWrite": "bash .claude/hooks/after-write.sh"
}
}
同时配置 BeforeWrite 与 AfterWrite
{
"hooks": {
"BeforeWrite": "node .claude/hooks/before-write.js",
"AfterWrite": "node .claude/hooks/after-write.js"
},
"permissions": {
"allow": [
"read",
"write"
]
}
}
配置建议
- 建议将钩子脚本存放在项目目录的
.claude/hooks/ 下统一管理
- 使用 Node.js 脚本可以获得更好的跨平台兼容性
- 钩子路径使用相对路径以便团队成员共享配置
- 全局配置适用于所有项目,项目级配置仅对当前项目生效
多级配置优先级
| 配置层级 |
配置文件路径 |
作用范围 |
| 项目级 |
.claude/settings.json |
仅当前项目 |
| 用户级 |
~/.claude/settings.json |
当前用户的所有项目 |
| 团队级(Git 管理) |
.claude/settings.local.json |
团队共享配置(不提交 .claude/) |
注意
项目级配置优先级最高,会覆盖用户级配置中相同钩子的定义。如果需要在项目级禁用某个全局钩子,可以将对应的钩子设置为空字符串 ""。
五、钩子脚本开发指南
钩子脚本是普通的可执行脚本,支持任何能在目标环境中运行的语言(Node.js、Python、Bash 等)。脚本通过标准输入(stdin)接收 JSON 格式的上下文信息,通过标准输出(stdout)返回结果。
脚本输入输出协议
钩子脚本与 Claude Code 之间通过标准输入输出进行通信,数据格式为 JSON。
输入格式(stdin 接收)
{
"filePath": "/path/to/target-file.js",
"content": "console.log('hello');",
"hookType": "BeforeWrite"
}
输出格式(stdout 返回)
BeforeWrite 钩子可以返回修改后的内容字符串,或返回原始内容:
console.log('hello world');
如果返回非字符串内容,或抛出异常,Claude Code 将视为验证失败并阻止写入。
Node.js 示例:自动备份脚本
const fs = require('fs');
const path = require('path');
// 从 stdin 读取 JSON 输入
let input = '';
process.stdin.on('data', chunk => { input += chunk; });
process.stdin.on('end', () => {
const { filePath, content } = JSON.parse(input);
// 如果文件已存在,创建备份
if (fs.existsSync(filePath)) {
const backupDir = path.join(path.dirname(filePath), '.backup');
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
const timestamp = Date.now();
const backupPath = path.join(backupDir,
path.basename(filePath) + '.' + timestamp + '.bak');
fs.copyFileSync(filePath, backupPath);
}
// 返回原始内容(不修改)
process.stdout.write(content);
});
Bash 示例:编码格式验证
#!/bin/bash
# 读取 stdin 中的 JSON 输入
input=$(cat)
filePath=$(echo "$input" | node -e "
let d='';process.stdin.on('data',c=>d+=c);
process.stdin.on('end',()=>console.log(JSON.parse(d).filePath));
")
content=$(echo "$input" | node -e "
let d='';process.stdin.on('data',c=>d+=c);
process.stdin.on('end',()=>console.log(JSON.parse(d).content));
")
# 检查文件是否包含非 UTF-8 字符
if echo "$content" | grep -P '[^\x00-\x7F\x80-\xFF\n\r\t]' > /dev/null; then
echo "Warning: File may contain non-UTF-8 characters" >&2
fi
# 透传原始内容
echo "$content"
开发建议
- 钩子脚本应尽量轻量快速,避免阻塞 Claude Code 的正常工作流
- 对于耗时操作(如远程备份),建议在 AfterWrite 中异步执行
- 脚本的错误处理要完善,避免因钩子异常导致写入被意外阻止
- 使用
process.stderr 输出调试信息,不会影响 stdout 的数据流
六、实际应用场景
BeforeWrite 和 AfterWrite 钩子在实际开发中有广泛的应用场景,覆盖编写安全、代码质量和自动化工作流等多个方面。
场景一:自动代码格式化
在 BeforeWrite 阶段对即将写入的代码进行自动化格式化,确保代码风格统一。
实现方案
- 使用 Prettier 或 ESLint 的自动修复功能对内容进行格式化
- 检测文件类型(如 .js, .ts, .css, .md),调用对应的格式化工具
- 返回格式化后的内容,保证团队代码风格一致
- 减少代码审查中关于格式的讨论,让审查聚焦于逻辑
场景二:自动备份机制
在文件被覆盖前自动创建备份,防止误操作导致的数据丢失。
实现方案
- 在 BeforeWrite 中检测目标文件是否存在
- 将现有文件复制到
.backup 目录,文件名附加时间戳
- 在 AfterWrite 中清理过期的备份文件,保留最近 N 个版本
- 配合 Git 使用时可减少"手滑"导致的内容丢失风险
场景三:敏感信息检测
在文件写入前检测是否包含敏感信息(API 密钥、密码、令牌等),阻止意外泄露。
实现方案
- 在 BeforeWrite 中使用正则表达式匹配常见敏感信息模式
- 匹配到敏感内容时抛出错误阻止写入
- 支持自定义敏感信息规则,适配不同项目的安全策略
- 与
.gitignore 配合形成多层防护
场景四:文件大小与类型限制
控制写入文件的大小和类型,防止生成过大的文件或写入不允许的文件类型。
实现方案
- 在 BeforeWrite 中检查 content 长度,超过阈值则阻止
- 校验文件扩展名是否在允许列表中
- 检测二进制内容,防止意外写入非文本文件
- 对日志文件和临时文件写入进行特殊处理
场景五:文件同步与部署触发
在文件写入后自动触发同步或部署流程。
实现方案
- 在 AfterWrite 中检测文件路径是否属于需要同步的目录
- 调用 rsync、scp 或其他同步工具将文件同步到远程服务器
- 触发 CI/CD 管道的 Webhook,通知构建系统有代码变更
- 记录文件变更日志,生成变更摘要
综合示例:完整的前后端项目防护
- BeforeWrite: 检测敏感信息、限制文件大小、自动格式化代码、创建备份
- AfterWrite: 运行 ESLint 检查、触发 TypeScript 编译、更新文件索引、通知部署系统
- 两者配合实现从写入前到写入后的全链路质量控制
七、最佳实践
编写健壮的钩子脚本
- 幂等性: 钩子脚本应可重复执行且结果一致,避免副作用累积
- 错误隔离: 钩子脚本的错误不应影响 Claude Code 主进程的稳定性
- 性能意识: 钩子脚本应快速执行(建议控制在 500ms 以内),避免影响开发体验
- 日志输出: 使用 stderr 输出日志,不要污染 stdout 的数据通道
- 版本控制: 将钩子脚本纳入 Git 管理,方便团队共享和追踪变更
推荐的钩子组合策略
- 轻量验证放 BeforeWrite: 文件大小检查、编码检测、敏感信息扫描
- 重量处理放 AfterWrite: 全量格式化、Lint 检查、远程同步、部署触发
- 备份操作放 BeforeWrite: 必须在覆盖前执行才能保留原始内容
- 通知操作放 AfterWrite: 写入成功后通知外部系统
开发工作流中的定位
| 阶段 |
操作 |
使用钩子 |
目的 |
| 编码前 |
项目初始化 |
— |
配置 hooks 和规则 |
| 编码中 |
文件写入 |
BeforeWrite |
验证 + 备份 + 格式化 |
| 编码后 |
写入完成 |
AfterWrite |
检查 + 同步 + 通知 |
| 提交前 |
Git 提交 |
pre-commit |
二次检查 |
八、注意事项与故障排除
常见陷阱
- 无限循环: 如果钩子脚本自身触发了 Claude Code 的文件写入,可能导致递归调用。建议在钩子内部添加递归检测或使用环境变量标记跳过
- 内容截断: 某些脚本语言对 stdin 输入有大小限制,大文件写入可能导致内容被截断。建议使用临时文件传递大内容
- 路径编码: Windows 和 Unix 系统的路径分隔符不同,钩子脚本应使用
path.join() 或 os.Path.join() 处理路径
- 权限问题: 钩子脚本的执行权限需要正确设置(Unix 下需
chmod +x)
故障排查步骤
- 检查配置文件: 确认
settings.json 中钩子路径正确,指向实际存在的脚本文件
- 验证脚本权限: 确保脚本文件有执行权限(Unix/Linux/Mac 系统)
- 测试脚本独立运行: 在命令行中手动调用钩子脚本,传入模拟 JSON 输入,验证输出是否符合预期
- 检查 stderr 输出: 查看 Claude Code 日志或控制台中是否有错误信息输出
- 检查依赖环境: 确认脚本依赖的运行时(Node.js、Python 等)已正确安装并在 PATH 中
- 逐步简化: 将脚本简化为最小可工作版本,逐步增加功能定位问题
调试钩子脚本的推荐方法
// 在钩子脚本中添加调试输出到 stderr
process.stderr.write(
JSON.stringify({
level: 'debug',
filePath: filePath,
contentLength: content.length,
timestamp: new Date().toISOString()
}) + '\n'
);
使用 stderr 输出调试信息不会干扰 Claude Code 对 stdout 的解析,是推荐的做法。
九、核心要点总结
BeforeWrite / AfterWrite 钩子速查表
| 维度 |
BeforeWrite |
AfterWrite |
| 触发时机 |
文件写入前 |
文件写入后 |
| 可阻止写入 |
是(返回错误或空值) |
否 |
| 可修改内容 |
是(返回修改后的内容) |
否 |
| 接收参数 |
filePath + content + hookType |
filePath + hookType |
| 典型用途 |
验证、备份、格式化 |
检查、同步、通知 |
| 性能要求 |
高(阻塞写入流程) |
中(不阻塞写入) |
记忆要点
- 写前防御: BeforeWrite 是文件写入的第一道防线,负责验证、备份和转换
- 写后处理: AfterWrite 是文件写入后的收尾环节,负责检查、同步和触发
- 配置优先: 所有钩子通过
settings.json 中的 hooks 字段配置
- JSON 协议: 钩子脚本通过 stdin/stdout 以 JSON 格式与 Claude Code 通信
- 轻量快速: 钩子脚本应保持轻量,避免影响开发体验
- 幂等安全: 钩子脚本应幂等执行,不产生副作用累积