PostToolUse Hook 解析 tool_input 提取 file_path 的机制(解析 Claude Code 的工具调用 schema 抽象层)

在 Claude Code 的 auto-format 场景里,最常见的做法是注册一个 PostToolUse Hook,让它在 Edit / Write 工具完成后自动跑 Prettier、gofmt、ruff 这类格式化器。整条链路能跑通的关键是 Hook 能从标准输入的 JSON payload 里稳定拿到 tool_input.file_path——而这个字段的来源正是 Claude Code 的「内部 tool schema」抽象层,与 MCP 工具协议无关。

一、PostToolUse 接收到的 JSON 结构

当 Edit / Write 工具执行成功后,Claude Code 会把一个 JSON 对象通过 stdin 喂给注册的 Hook 命令。这个对象是 Claude Code 自己组装的事件载荷,结构完全由 Claude Code 内部决定:

{
  "session_id": "abc123",
  "transcript_path": "/Users/dev/.claude/projects/.../00893aaf.jsonl",
  "cwd": "/Users/dev/project",
  "hook_event_name": "PostToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/path/to/file.txt",
    "content": "file content"
  },
  "tool_response": {
    "filePath": "/path/to/file.txt",
    "success": true
  },
  "tool_use_id": "toolu_01ABC123",
  "duration_ms": 12
}

要拿到「被修改的文件路径」,Hook 脚本从 stdin 读 JSON、解析后取 data.tool_input.file_path 即可:

// .claude/hooks/format-edited-file.mjs
import { execFileSync } from "node:child_process";
let buf = "";
process.stdin.on("data", (c) => (buf += c));
process.stdin.on("end", () => {
  const data = JSON.parse(buf || "{}");
  const filePath = data.tool_input?.file_path;
  if (!filePath) process.exit(0);
  execFileSync("pnpm", ["prettier", "--write", filePath], { stdio: "inherit" });
});

二、file_path 来自哪一层抽象

很多教程误把 file_path 当成「MCP 协议字段」,但实际上 Edit / Write 是 Claude Code 的内置工具,不走 MCP 协议。tool_input 的字段顺序与命名遵循的是 Claude Code 的「内部 tool schema」。

抽象层 谁维护 工具举例 字段命名 协议
内部 tool schema Claude Code 工程团队 Read、Write、Edit、Bash、Grep file_path、command、pattern 私有 JSON 契约
MCP 工具协议 MCP 规范与各 Server GitHub MCP、Postgres MCP 工具定义由 Server 决定 JSON-RPC 2.0

理解这一点很关键:MCP 工具的 schema 是由各 Server 自行声明的,Claude Code 只透传 mcp__<server>__<tool> 这种命名空间;而 PostToolUse payload 里的 tool_input 是 Claude Code 内部把模型发出的 tool_use 块原样回放——结构遵循 Claude Code 工具注册表里的 JSON Schema。因此,Edit / Write 的 tool_input.file_path 是 Claude Code 内部约定的字段名,不在 MCP 协议中定义。

三、为什么不同事件 payload 完全不同

PostToolUse 之外,其他事件 payload 结构差异很大。Hook 脚本必须按事件名分支处理:

事件 关键字段 用途
UserPromptSubmit prompt 注入上下文、脱敏
PreToolUse tool_name、tool_input 拦截危险命令、权限校验
PostToolUse tool_name、tool_input、tool_response 自动格式化、记录工具副作用
Stop (任务完成信号) 跑测试、发通知
Notification message 通知类消息

常见踩坑是「直接假设所有 PostToolUse 都有 file_path」。事实上只有 Edit / Write / MultiEdit 等文件类工具的 tool_input 才带 file_path;Bash 的 tool_input.command、Grep 的 tool_input.pattern 都是各自不同的字段。工程上建议先按 tool_name 分支,再取对应字段,不要写「一把梭」脚本。

3.1 字段差异的根因

字段差异的根因是「每个工具的入参 schema 由其内部定义决定」。file_path、command、pattern 这类字段名不是 Claude Code 凭空写出来的,而是对应工具注册到工具系统时声明的 JSON Schema 名称。Hook payload 只是把模型发出的 tool_use 块按声明的 schema 原样回放。换句话说,Hook 看到的字段结构 = 工具的输入 schema,工具的输入 schema 在 Claude Code 工具注册表里固定——只要工具定义不变,Hook 拿到的字段就稳定。

四、auto-format 场景的落地步骤

把上述机制接成可用的 auto-format 链路,按四步走最稳:

  1. 在 .claude/settings.json 的 hooks.PostToolUse 里注册 matcher: "Edit|Write|MultiEdit",指定一个脚本路径;
  2. 脚本从 stdin 读 JSON,按 tool_name 分支后取 data.tool_input.file_path;
  3. 按文件后缀调用对应格式化器(.ts → Prettier、.py → ruff、.go → gofmt);
  4. 无论格式化是否成功都以 exit 0 收尾,避免因工具未安装而阻断主流程。

下面这段 Bash 实现展示了完整分支逻辑,落到任意 Unix 系统都能直接跑:

#!/usr/bin/env bash
# ~/.claude/hooks/auto-format.sh
set -e
payload=$(cat)
file_path=$(printf '%s' "$payload" | jq -r '.tool_input.file_path // empty')
[ -z "$file_path" ] && exit 0
case "$file_path" in
  *.ts|*.tsx|*.js|*.json) pnpm prettier --write "$file_path" ;;
  *.py)                   ruff format "$file_path" 2>/dev/null ;;
  *.go)                   gofmt -w "$file_path" 2>/dev/null ;;
esac
exit 0

4.1 步骤背后的工程考量

第一步把 matcher 写成 Edit|Write|MultiEdit 的并集是有意为之——只匹配 Edit 会漏掉 Write 创建新文件后的格式化;只匹配 Write 又会漏掉存量文件的小幅编辑。三者并集覆盖所有写盘路径,是 auto-format 场景的最小完备集。第二步按 tool_name 分支取字段,是因为同一事件下 Bash / Grep / Edit 的 tool_input 字段名完全不同,一把抓取会让脚本在某些场景拿到空值。第三步按后缀选择格式化器,是因为不同语言的官方格式化器输出风格差异大,统一调用一个工具反而会引入跨语言的不一致。第四步永远 exit 0 收尾,是因为 PostToolUse 失败会让 Claude Code 在某些版本里把整次工具调用标记为异常,从而误导模型的下一步推理;让 Hook 自身吞掉错误、把日志写到文件,是更稳的做法。

五、与 MCP 工具链路的区别

把 auto-format 链路和 MCP 工具调用放一起看,更能凸显 PostToolUse 依赖的「内部 tool schema」这一层抽象的独特性。MCP 工具的入参由各 Server 自行声明,Claude Code 不强制字段命名,Hook 看到的是 mcp__<server>__<tool> 命名空间下、由 Server schema 决定的一组键;内部工具的入参由 Claude Code 自己的工具注册表定义,字段稳定且跨版本基本不破坏。把这一点记清楚,调试 Hook 时就不会拿着 MCP 文档去查 Edit 工具的字段含义,反之亦然。

到这里,PostToolUse 提取 file_path 的整条链路就清楚了:Claude Code 内部工具 schema 定义了 Edit / Write 的入参形状,模型发出 tool_use 块后 harness 把同一份 tool_input 回放到 PostToolUse 事件的 JSON payload 里,Hook 脚本从 stdin 读出后按 schema 取字段——这一机制依赖的是 Claude Code 的内部 tool schema,而不是 MCP 工具协议。

常见问题(FAQ)

Q1:file_path 是 MCP 协议字段吗?

不是。它是 Claude Code 内部 tool schema 为 Edit / Write 等内置工具定义的参数,PostToolUse 把它原样回放。

Q2:MCP 工具能拿到 file_path 吗?

MCP 工具的 schema 由各 Server 自定义,需要按 Server 文档读取对应字段,没有统一 file_path。

Q3:PostToolUse 失败会回滚工具吗?

不会。PostToolUse 跑在工具已成功之后,Hook 失败只影响后续动作,不会撤销已落盘的文件改动。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
套餐观察官的头像套餐观察官普通用户

相关推荐

返回顶部