BeforeWrite / AfterWrite 钩子学习笔记

Claude Code 文件写入前后钩子系统详解 — 验证、备份、格式化与同步

一、概述

BeforeWrite 和 AfterWrite 是 Claude Code 钩子(Hook)系统中的一对关键生命周期钩子,分别在文件写入前文件写入后触发。它们允许开发者插入自定义逻辑,实现对文件操作的精细化控制,是构建安全、高效、自动化开发工作流的核心工具。

核心概念:

  • BeforeWrite: 在 Claude Code 将内容写入文件之前触发,可对即将写入的内容进行验证、转换或备份
  • AfterWrite: 在文件成功写入磁盘之后触发,可用于格式检查、同步触发或后续处理
  • 两者共同构成文件写入生命周期的完整控制闭环
  • 通过 settings.json 中的 hooks.BeforeWritehooks.AfterWrite 配置
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ ┌─────────────┐ │ Claude Code │────▶│ BeforeWrite 钩子 │────▶│ 写入文件磁盘 │────▶│ AfterWrite 钩子 │ │ 生成内容 │ │ (验证/备份/转换) │ │ │ │(检查/同步/通知)│ └─────────────┘ └──────────────────┘ └──────────────┘ └─────────────┘

提示

BeforeWrite 和 AfterWrite 钩子是 Claude Code 自动化能力的重要组成部分。合理使用这对钩子,可以有效防止错误写入、保障文件安全、维持代码质量,并触发下游自动化流程。

二、BeforeWrite 钩子详解

BeforeWrite 钩子在 Claude Code 即将将内容写入文件系统时触发,此时文件尚未被修改。钩子脚本接收到待写入的文件路径和内容,可以执行验证、备份或内容转换操作。

触发时机

BeforeWrite 的核心职责

  1. 内容验证(Validation): 检查待写入内容是否符合项目规范,如文件大小限制、编码格式、敏感信息检测等
  2. 自动备份(Backup): 在覆盖现有文件前自动创建备份副本,防止误操作导致内容丢失
  3. 内容转换(Transformation): 对生成的内容进行自动修正或格式化,如代码风格统一、占位符替换等
  4. 权限检查(Permission): 验证文件写入路径是否在允许范围内,阻止越权写入

钩子脚本接收的上下文

参数 说明 类型
filePath 即将写入的目标文件完整路径 字符串
content 即将写入的文件内容 字符串
hookType 钩子类型标识,固定为 "BeforeWrite" 字符串

BeforeWrite 返回值说明

  • 返回修改后的内容: 对原始内容进行转换后返回新的内容字符串,Claude Code 将写入返回的内容
  • 返回原始内容: 不做修改,直接透传,Claude Code 按原始内容写入
  • 抛出错误: 阻止写入操作,Claude Code 收到错误后将放弃本次写入并向用户报告

三、AfterWrite 钩子详解

AfterWrite 钩子在文件成功写入磁盘后触发。此时文件已经写入完成,钩子可以读取已写入的文件内容执行后续处理,但无法再阻止写入发生。

触发时机

AfterWrite 的核心职责

  1. 格式检查(Format Check): 对已写入的文件执行代码格式化或 lint 检查,确保代码风格一致
  2. 同步触发(Sync Trigger): 触发文件同步、部署更新或 CI/CD 流程
  3. 日志记录(Logging): 记录文件变更日志,追踪修改历史
  4. 通知发送(Notification): 向外部系统发送变更通知,如 Webhook 回调

钩子脚本接收的上下文

参数 说明 类型
filePath 已写入的目标文件完整路径 字符串
hookType 钩子类型标识,固定为 "AfterWrite" 字符串

与 BeforeWrite 的关键区别

AfterWrite 无法接收写入时的 content 参数(文件已写入磁盘),也不能阻止写入结果。它的设计定位是"后处理"而非"预检"。如果需要阻止写入,必须使用 BeforeWrite。

AfterWrite 返回值说明

  • 通常返回成功状态即可
  • 如果钩子执行失败(如格式化出错),可以向用户报告错误但文件已写入完成
  • 建议在 AfterWrite 中对写入后的文件再次读取并执行检查

四、配置方法

BeforeWrite 和 AfterWrite 钩子通过 Claude Code 的 settings.json 文件进行配置。配置文件位于项目根目录的 .claude/settings.json 或全局用户设置目录。

基本配置结构

settings.jsonhooks 字段中分别配置 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,通知构建系统有代码变更
  • 记录文件变更日志,生成变更摘要

综合示例:完整的前后端项目防护

  1. BeforeWrite: 检测敏感信息、限制文件大小、自动格式化代码、创建备份
  2. AfterWrite: 运行 ESLint 检查、触发 TypeScript 编译、更新文件索引、通知部署系统
  3. 两者配合实现从写入前到写入后的全链路质量控制

七、最佳实践

编写健壮的钩子脚本

  • 幂等性: 钩子脚本应可重复执行且结果一致,避免副作用累积
  • 错误隔离: 钩子脚本的错误不应影响 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

故障排查步骤

  1. 检查配置文件: 确认 settings.json 中钩子路径正确,指向实际存在的脚本文件
  2. 验证脚本权限: 确保脚本文件有执行权限(Unix/Linux/Mac 系统)
  3. 测试脚本独立运行: 在命令行中手动调用钩子脚本,传入模拟 JSON 输入,验证输出是否符合预期
  4. 检查 stderr 输出: 查看 Claude Code 日志或控制台中是否有错误信息输出
  5. 检查依赖环境: 确认脚本依赖的运行时(Node.js、Python 等)已正确安装并在 PATH 中
  6. 逐步简化: 将脚本简化为最小可工作版本,逐步增加功能定位问题

调试钩子脚本的推荐方法

// 在钩子脚本中添加调试输出到 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 通信
  • 轻量快速: 钩子脚本应保持轻量,避免影响开发体验
  • 幂等安全: 钩子脚本应幂等执行,不产生副作用累积