BeforeEdit / AfterEdit — 文件编辑前后 Hook

Claude Code Hook 详解 · 编辑生命周期管理

一、Hook 概述

BeforeEdit 和 AfterEdit 是 Claude Code 提供的一对编辑生命周期 Hook,分别在文件内容被实际写入磁盘之前和之后触发。它们构成了文件编辑操作的"安检门"机制:BeforeEdit 负责前置检查与防护,AfterEdit 负责后置验证与清理。两者配合使用,能够在保证编辑操作安全性的同时,自动执行代码质量检查、格式校验、合规审计等一系列任务,是 Claude Code 自动化工作流中极其重要的组成部分。

与 onStart、onStop 等会话级别的 Hook 不同,BeforeEdit 和 AfterEdit 是操作级别的细粒度 Hook,每次 Claude 执行文件编辑工具(如 Edit、Write)时都会触发。这意味着它们可以被高频调用,适用于那些需要在每次编辑时都执行的检查和验证任务。这种设计使得开发者可以在不增加人工干预的前提下,确保每一次代码变更都符合项目规范。

作为一对互补的 Hook,BeforeEdit 和 AfterEdit 的设计哲学是"预防优于治疗,验证保障质量"。BeforeEdit 侧重于在变更发生前发现问题并阻止有害操作,AfterEdit 则侧重于在变更发生后验证结果并确保代码库的完整性。两者结合,形成了一个完整的编辑质量保障闭环,让代码编辑过程变得可控、可追溯、可验证。

二、BeforeEdit 详解

2.1 触发时机与工作原理

BeforeEdit Hook 在 Claude Code 即将执行文件编辑操作之前触发。具体而言,当 Claude 调用 Edit 或 Write 工具修改文件内容时,系统会在实际写入磁盘之前暂停操作,执行 BeforeEdit 中配置的所有脚本或命令。只有当这些脚本全部执行成功(即返回退出码 0)时,编辑操作才会继续执行;如果任何一个脚本执行失败,编辑操作将被取消,文件内容不会被修改。

这种"先检查后写入"的机制为编辑操作提供了关键的安全屏障。BeforeEdit 接收环境变量 $CLAUDE_FILE 和 $CLAUDE_CONTENT,分别表示被编辑文件的路径和即将写入的新内容。利用这些信息,Hook 脚本可以在编辑发生前对目标文件和即将写入的内容进行全面的分析和验证。

2.2 差异检查(Diff Check)

差异检查是 BeforeEdit 最常用的功能之一。Hook 脚本可以计算当前文件内容与即将写入的新内容之间的差异,识别出所有新增、删除和修改的行。这对于防止误操作尤为重要——例如,当用户无意中删除了关键配置段或修改了不可变文件时,BeforeEdit 可以通过差异分析检测到异常并阻止编辑。

#!/bin/bash # BeforeEdit - 差异检查脚本 DIFF=$(diff "$CLAUDE_FILE" - <<< "$CLAUDE_CONTENT") if [ $? -eq 0 ]; then echo "警告:文件内容未发生变化,编辑被取消" exit 1 fi CHANGED_LINES=$(echo "$DIFF" | grep -c '^[<>]') if [ "$CHANGED_LINES" -gt 100 ]; then echo "错误:单次编辑超过 100 行变更限制,已阻止" exit 1 fi exit 0

2.3 自动备份(Auto Backup)

在编辑发生前创建备份是一种良好的防御性编程实践。BeforeEdit 可以在执行编辑前自动将原始文件内容复制到备份目录,确保在出现意外时能够快速恢复。备份文件通常以原始文件名加时间戳的方式命名,便于后续检索和恢复。

#!/bin/bash # BeforeEdit - 自动备份脚本 BACKUP_DIR=".claude/backups/$(dirname "$CLAUDE_FILE")" mkdir -p "$BACKUP_DIR" TIMESTAMP=$(date +"%Y%m%d%H%M%S") BASENAME=$(basename "$CLAUDE_FILE") cp "$CLAUDE_FILE" "$BACKUP_DIR/${BASENAME}.${TIMESTAMP}.bak" echo "已备份 $CLAUDE_FILE 到 $BACKUP_DIR/${BASENAME}.${TIMESTAMP}.bak" exit 0

2.4 安全检查与权限验证

BeforeEdit 还可以用于执行安全检查,验证当前编辑操作是否在授权范围内。例如,可以配置 BeforeEdit 来阻止对敏感配置文件(如 .env、credentials.json、settings.json)的修改,或者验证编辑者是否具有足够的权限来修改特定目录下的文件。这种安全检查机制为团队协作环境提供了额外的安全保障。

三、AfterEdit 详解

3.1 触发时机与工作原理

AfterEdit Hook 在 Claude Code 成功完成文件编辑操作之后触发。与 BeforeEdit 不同,AfterEdit 在文件写入完成后执行,因此可以读取编辑后的文件内容进行验证。AfterEdit 同样接收 $CLAUDE_FILE 环境变量,但需要注意的是,它不接收 $CLAUDE_CONTENT,因为在触发时内容已经写入磁盘,可以直接从文件中读取。

AfterEdit 的执行结果不会影响编辑操作本身——即使 AfterEdit 中的脚本执行失败,已经写入磁盘的文件也不会被自动回滚。这意味着 AfterEdit 更适合用于非阻塞的验证和通知任务,例如生成警告、记录日志、触发后续流程等。如果需要阻止有害的编辑操作,应当在 BeforeEdit 中实现。

3.2 语法验证(Syntax Validation)

语法验证是 AfterEdit 最核心的应用场景之一。编辑完成后,AfterEdit 可以自动调用相应的编译器或解释器来验证修改后的文件语法是否正确。对于 TypeScript 项目可以运行 tsc --noEmit,对于 Python 项目可以运行 python -m py_compile,对于 JSON 文件可以运行 python -m json.tool。这样可以确保每次编辑都不会引入语法错误。

#!/bin/bash # AfterEdit - Python 语法验证 if [[ "$CLAUDE_FILE" == *.py ]]; then python -m py_compile "$CLAUDE_FILE" if [ $? -ne 0 ]; then echo "警告:Python 语法错误 detected in $CLAUDE_FILE" else echo "Python 语法验证通过:$CLAUDE_FILE" fi fi exit 0

3.3 合规检查(Compliance Check)

合规检查是 AfterEdit 的另一重要功能。Hook 脚本可以扫描编辑后的文件,检查其是否符合项目约定的编码规范、命名规则、文件大小限制、行数限制等。例如,可以检查是否所有新增的 import 语句都按字母顺序排列,是否所有函数都有对应的类型注解,文件长度是否超过预设阈值等。

#!/bin/bash # AfterEdit - 合规检查 MAX_LINES=500 FILE_EXT="${CLAUDE_FILE##*.}" LINE_COUNT=$(wc -l < "$CLAUDE_FILE") if [ "$LINE_COUNT" -gt "$MAX_LINES" ]; then echo "合规警告:$CLAUDE_FILE 行数 $LINE_COUNT 超过限制 $MAX_LINES" fi if grep -n "TODO\|FIXME\|HACK" "$CLAUDE_FILE" > /dev/null 2>&1; then echo "合规提示:$CLAUDE_FILE 中包含待办标记(TODO/FIXME/HACK)" fi exit 0

3.4 变更日志记录

AfterEdit 可以将每次编辑操作的信息记录到变更日志中,包括编辑时间、文件路径、编辑摘要等。这对于审计追踪和问题排查非常有价值。日志可以采用结构化的格式(如 JSON Lines),便于后续的分析和处理。

#!/bin/bash # AfterEdit - 变更日志记录 LOG_DIR=".claude/changelogs" mkdir -p "$LOG_DIR" LOG_FILE="$LOG_DIR/$(date +"%Y%m%d").jsonl" echo "{\"time\":\"$(date -Iseconds)\",\"file\":\"$CLAUDE_FILE\",\"action\":\"edit\"}" >> "$LOG_FILE" exit 0

四、配置指南

4.1 配置文件结构

BeforeEdit 和 AfterEdit 的配置位于项目的 .claude/settings.json 或用户级别的 ~/.claude/settings.json 文件中。它们作为 hooks 对象的子属性进行配置,与 onStart、onStop 等会话级 Hook 位于同一层级。每个 Hook 可以配置多个脚本或命令,系统会按照配置顺序依次执行。

{ "hooks": { "BeforeEdit": [ "bash .claude/hooks/before-edit/backup.sh", "bash .claude/hooks/before-edit/diff-check.sh", "bash .claude/hooks/before-edit/security-check.sh" ], "AfterEdit": [ "bash .claude/hooks/after-edit/syntax-validate.sh", "bash .claude/hooks/after-edit/compliance-check.sh", "bash .claude/hooks/after-edit/changelog.sh" ] } }

4.2 环境变量说明

变量名适用 Hook说明
$CLAUDE_FILEBeforeEdit / AfterEdit被编辑文件的完整路径
$CLAUDE_CONTENTBeforeEdit即将写入的新文件内容(AfterEdit 中不可用)
$CLAUDE_PROJECT_ROOTBeforeEdit / AfterEdit项目根目录路径

$CLAUDE_CONTENT 仅在 BeforeEdit 中可用,因为 AfterEdit 触发时内容已经写入磁盘。这意味着 BeforeEdit 可以在不修改磁盘的情况下对新内容进行预检,而 AfterEdit 则需要在写入后重新读取文件进行验证。

4.3 配置层级与优先级

与 Claude Code 的其他配置一样,BeforeEdit 和 AfterEdit 的配置遵循层级优先级规则:项目级配置(.claude/settings.json)优先级最高,用户级配置(~/.claude/settings.json)次之。当多个层级的配置中存在相同的 Hook 定义时,项目级配置会覆盖用户级配置。需要注意的是,Hook 配置不会像权限那样被合并——子级配置会完全替换父级配置中的同名 Hook 定义。

五、配置示例

5.1 完整的前后端验证示例

以下是一个完整的 BeforeEdit/AfterEdit 配置示例,涵盖了差异检查、自动备份、语法验证和合规检查等常见场景。

{ "hooks": { "BeforeEdit": [ "echo '=== BeforeEdit Hook 开始 ==='", "bash tools/hooks/before-backup.sh", "bash tools/hooks/before-diff-check.sh", "echo '=== BeforeEdit Hook 结束 ==='" ], "AfterEdit": [ "echo '=== AfterEdit Hook 开始 ==='", "bash tools/hooks/after-syntax-check.sh", "bash tools/hooks/after-lint-check.sh", "echo '=== AfterEdit Hook 结束 ==='" ] } }

5.2 语言特定的 Hook 配置

根据项目使用的编程语言,可以配置特定语言对应的验证脚本。以下示例展示了如何为 Python 和 TypeScript 项目分别配置不同的语法验证逻辑。

{ "hooks": { "AfterEdit": [ "bash scripts/validate-file.sh" ] } }

在 validate-file.sh 脚本中,可以根据文件扩展名调用不同的验证工具:

#!/bin/bash EXT="${CLAUDE_FILE##*.}" case "$EXT" in py) python -m py_compile "$CLAUDE_FILE" ;; ts|tsx) npx tsc --noEmit --skipLibCheck "$CLAUDE_FILE" 2>/dev/null ;; json) python -m json.tool "$CLAUDE_FILE" > /dev/null 2>&1 ;; yaml|yml) python -c "import yaml; yaml.safe_load(open('$CLAUDE_FILE'))" 2>/dev/null ;; esac exit 0

5.3 分环境配置

对于在不同环境下有不同验证要求的项目,可以通过环境变量或条件判断来区分处理逻辑。例如,在开发环境中可以放宽检查标准,在生产环境中则执行严格的验证。

#!/bin/bash # 根据环境变量调整验证严格程度 if [ "$NODE_ENV" = "production" ]; then # 生产环境:严格验证模式 npx eslint --max-warnings 0 "$CLAUDE_FILE" else # 开发环境:宽松验证模式 npx eslint "$CLAUDE_FILE" 2>/dev/null || true fi exit 0

六、应用场景

6.1 自动代码检查(Auto Linting)

在 AfterEdit 中集成 ESLint、Pylint、RuboCop 等 lint 工具,可以在每次编辑后自动执行代码风格检查。这种方式比传统的保存后手动运行 lint 更加高效,因为检查是在编辑行为发生的瞬间自动完成的,开发者无需中断工作流。lint 检查的结果可以以警告的形式输出,不会阻止编辑操作,但会提醒开发者注意代码质量问题。

#!/bin/bash # AfterEdit - 自动代码检查 EXT="${CLAUDE_FILE##*.}" case "$EXT" in js|jsx|ts|tsx) npx eslint --quiet "$CLAUDE_FILE" 2>/dev/null if [ $? -ne 0 ]; then echo "Lint 提醒:$CLAUDE_FILE 存在代码质量问题,建议检查" fi ;; py) pylint --score=n "$CLAUDE_FILE" 2>/dev/null || echo "Lint 提醒:请检查 Python 代码风格" ;; esac exit 0

6.2 代码格式化检查(Code Format Check)

代码格式化检查是团队协作中防止风格冲突的重要工具。在 AfterEdit 中集成 Prettier、Black 或 gofmt 等格式化工具,可以验证编辑后的代码是否符合项目约定的格式标准。与自动格式化不同,这里执行的是检查而非修改——如果格式不符合标准,Hook 会发出警告并建议运行格式化命令。

#!/bin/bash # AfterEdit - 代码格式化检查 EXT="${CLAUDE_FILE##*.}" case "$EXT" in js|jsx|ts|tsx|json|css|md) npx prettier --check "$CLAUDE_FILE" 2>/dev/null if [ $? -ne 0 ]; then echo "格式提醒:$CLAUDE_FILE 格式不符合 Prettier 规范,请运行 npx prettier --write" fi ;; py) black --check --quiet "$CLAUDE_FILE" 2>/dev/null if [ $? -ne 0 ]; then echo "格式提醒:$CLAUDE_FILE 格式不符合 Black 规范,请运行 black" fi ;; esac exit 0

6.3 敏感文件保护

在某些项目中,部分文件不应被随意修改,例如配置文件、锁文件(package-lock.json、yarn.lock)、环境变量模板等。BeforeEdit 可以通过文件路径匹配来检测这些敏感文件的编辑请求,并在必要时阻止操作。这种保护机制可以有效防止误操作导致的项目配置损坏或安全信息泄露。

#!/bin/bash # BeforeEdit - 敏感文件保护 SENSITIVE_FILES=( ".env" "package-lock.json" "yarn.lock" "credentials.json" ".claude/settings.json" ) for SENSITIVE in "${SENSITIVE_FILES[@]}"; do if [[ "$CLAUDE_FILE" == *"$SENSITIVE" ]]; then echo "错误:$SENSITIVE 是受保护文件,禁止编辑" exit 1 fi done exit 0

6.4 编辑频率限制

为了防止误操作导致的大量文件修改,可以在 BeforeEdit 中实现编辑频率限制逻辑。例如,限制每分钟内的编辑次数,或限制连续编辑的文件数量。这在批量编辑场景中特别有用,可以有效避免因脚本逻辑错误导致的连锁修改。

#!/bin/bash # BeforeEdit - 编辑频率限制 RATE_LIMIT_FILE=".claude/.edit_rate_limit" CURRENT_TIME=$(date +%s) if [ -f "$RATE_LIMIT_FILE" ]; then LAST_EDIT_TIME=$(cat "$RATE_LIMIT_FILE") TIME_DIFF=$((CURRENT_TIME - LAST_EDIT_TIME)) if [ "$TIME_DIFF" -lt 2 ]; then echo "警告:编辑过于频繁,请稍后再试" exit 1 fi fi echo "$CURRENT_TIME" > "$RATE_LIMIT_FILE" exit 0

七、最佳实践与设计模式

7.1 幂等性设计

BeforeEdit 和 AfterEdit 的脚本应当具备幂等性,即无论执行多少次,其结果都应该是一致的。这是因为在某些情况下,Hook 可能会被重复触发(例如,编辑操作失败后重试)。幂等性设计可以通过检查目标状态是否存在、使用锁文件、保证操作的原子性等方式实现。

7.2 快速失败原则

BeforeEdit 脚本应当遵循快速失败原则,将最有可能失败且检查成本最低的检查放在最前面。例如,文件扩展名检查应当优先于语法检查。这样可以在早期阶段就发现并阻止无效的编辑操作,避免执行后续的计算密集型检查。快速失败能够显著提升 Hook 的执行效率,减少每次编辑的等待时间。

7.3 非阻塞验证

AfterEdit 中的验证应当是非阻塞的,不应阻止编辑操作的完成。如果验证发现问题,应当以警告或日志的形式记录,而不是尝试修改文件或回滚操作。非阻塞验证的设计理念是尊重开发者的操作意图,同时提供有价值的质量反馈。这种设计使得开发者可以根据实际情况决定是否处理警告,而不是被强制中断工作流。

设计原则总结

1. BeforeEdit 负责前置防护:备份原始文件、检查差异大小、验证操作权限。

2. AfterEdit 负责后置验证:语法检查、合规审计、变更日志记录。

3. BeforeEdit 失败会阻止编辑,AfterEdit 失败不影响已写入的文件。

4. 所有 Hook 脚本应当保持轻量高效,避免长时间运行阻塞编辑操作。

5. 充分利用 $CLAUDE_FILE 和 $CLAUDE_CONTENT 环境变量传递上下文信息。

八、故障排除指南

8.1 Hook 未触发

如果 BeforeEdit 或 AfterEdit 未按预期触发,首先检查配置文件的语法是否正确。可以使用 jsonlint 或类似的 JSON 验证工具检查 settings.json 的格式。其次确认 Hook 名称的大小写是否正确——在 settings.json 中,Hook 名称使用驼峰命名法(BeforeEdit、AfterEdit),注意不要写成 "beforeEdit"、"before_edit" 或其他变体。最后检查配置文件是否位于正确的位置(项目级的 .claude/settings.json 或用户级的 ~/.claude/settings.json)。

8.2 Hook 执行失败

当 Hook 执行失败时,Claude Code 会在日志中记录错误信息。常见失败原因包括:Hook 脚本路径不正确(使用相对路径时要从项目根目录解析);脚本执行权限不足(需要 chmod +x);环境变量缺失(某些脚本依赖特定的环境变量);命令或工具未安装(如 eslint、pylint 等不在 PATH 中)。建议在 Hook 脚本中加入详细的错误输出和调试信息,便于快速定位问题。

8.3 BeforeEdit 误阻止

偶尔会出现 BeforeEdit 误阻止合法编辑操作的情况——例如,差异检查脚本的安全阈值设置过低,将正常的批量修改误判为异常操作。此时可以暂时禁用相关 Hook 进行检查。在确认 Hook 脚本的逻辑正确性后,可以调整阈值参数或增加例外规则。建议在开发初期使用宽松的检查策略,待稳定后再逐步收紧。

8.4 AfterEdit 验证延迟

AfterEdit 中的验证脚本如果执行时间过长(例如,运行完整的测试套件),可能会显著影响编辑体验。解决方案是将耗时的验证任务异步化——在 AfterEdit 中触发后台进程来执行耗时检查,而不是同步等待其结果。另一个方案是将验证分为快速验证和深度验证两个阶段,快速验证在 AfterEdit 中同步执行,深度验证则通过 onStop 或定时任务异步执行。

九、核心总结

BeforeEdit 和 AfterEdit 是 Claude Code 文件编辑操作的安全保障与质量守门员。BeforeEdit 在编辑发生前执行备份、差异检查和权限验证,一旦脚本执行失败即取消编辑操作,为文件内容提供前置保护;AfterEdit 在编辑完成后执行语法验证、合规检查和变更记录,以非阻塞方式提供质量反馈。两者配合使用,构建了一个完整的编辑生命周期管理闭环。核心要点:BeforeEdit 可阻止编辑而 AfterEdit 仅可发出警告;$CLAUDE_CONTENT 仅在 BeforeEdit 中可用;Hook 脚本应遵循幂等性、快速失败和非阻塞原则;适用于自动代码检查、格式化验证、敏感文件保护和编辑频率限制等多种场景。掌握 BeforeEdit 和 AfterEdit 的配置与使用,是充分发挥 Claude Code 自动化能力的关键一步。