# Hooks：编辑后自动格式化

> 用 PostToolUse hook 在 Claude 每次编辑或写入文件后自动运行 Prettier 或 ruff，保证格式稳定，附可处理空格路径与多语言的脚本版本。

## 适用场景

你在 CLAUDE.md 里写了「改完文件请运行格式化」，Claude 大部分时候照做，偶尔忘记，于是提交里混进了格式变动。这类「每次必须发生」的事不该靠提醒，而应该交给 hook。

hook 是 Claude Code 在生命周期的固定节点上运行的 shell 命令，不经过模型决策。把格式化挂在「文件编辑工具执行成功之后」这个节点，就能保证每次改动都被格式化。

## 模板

写入项目的 `.claude/settings.json`。这个版本直接用 `jq` 取出被编辑文件的路径，交给 Prettier：

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write --ignore-unknown"
          }
        ]
      }
    ]
  }
}
```

## 怎么用 / 怎么改

- **前提。** 机器上要有 `jq`（macOS 用 `brew install jq`，Debian 与 Ubuntu 用 `apt-get install jq`），项目里要有 Prettier。
- **工作原理。** 事件触发时，Claude Code 把事件数据以 JSON 通过标准输入交给你的命令。`PostToolUse` 的 `matcher` 按工具名过滤，`Edit|Write` 表示只在编辑或写入文件后触发；`tool_input.file_path` 就是被改的文件。
- **验证方法。** 让 Claude 在某个 JS 文件里加一行用单引号的字符串，然后打开文件，看是否被改成了 Prettier 默认的双引号。hook 成功时界面上不会提示任何内容。
- **文件已生效。** 直接编辑 settings 文件，运行中的会话会自动重新加载；运行 `/hooks` 可以浏览已配置的所有 hook。

### 脚本版：处理空格路径与多语言

路径含空格时 `xargs` 会拆错，也无法按文件类型选择工具。改用脚本。保存为 `.claude/hooks/format.sh`，并执行 `chmod +x .claude/hooks/format.sh`：

```bash
#!/bin/bash
FILE=$(jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0

case "$FILE" in
  *.ts|*.tsx|*.js|*.jsx|*.json|*.css|*.md|*.astro)
    npx prettier --write "$FILE" >/dev/null 2>&1 || true
    ;;
  *.py)
    ruff format "$FILE" >/dev/null 2>&1 || true
    ;;
esac
exit 0
```

然后把 settings 里的 `command` 换成脚本，还可以加超时和提示文字：

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format.sh",
            "timeout": 30,
            "statusMessage": "正在格式化…"
          }
        ]
      }
    ]
  }
}
```

## 常见问题或注意事项

- **脚本必须可执行。** 忘记 `chmod +x` 是最常见的原因。hook 没触发时，先用 `claude --debug` 或会话里的 `/debug` 查看日志。
- **Bash 命令改的文件不会触发。** 如果 Claude 用 `sed` 或重定向改了文件，`Edit|Write` 的匹配不会命中。要覆盖这种情况，可以改用 `FileChanged` 事件监听特定文件，或者在 `Stop` 事件里每轮结束后统一格式化。
- **退出码有含义。** 退出 0 表示没有异议；退出 2 表示阻止，并把标准错误输出作为反馈交给 Claude。其他非零退出码在多数事件里只会让操作继续并给出一条 hook 错误提示。格式化 hook 不应该阻止任何事，所以示例脚本总是 `exit 0`。
- **想拦截而不是整理。** 例如禁止修改 `.env`，应该用 `PreToolUse` 事件，脚本检查 `tool_input.file_path`，命中就写标准错误并 `exit 2`。官方指南有完整示例。
- **团队共享要保证工具存在。** 把 `jq`、Prettier 的安装步骤写进 README 或 CLAUDE.md，否则同事的 hook 会静默失败。
- **worktree 中的路径。** 脚本用 `$CLAUDE_PROJECT_DIR` 引用，它始终指向启动会话的项目根；而被编辑的文件路径来自输入 JSON，所以格式化的是正确的文件。