PreToolUse:工具执行前的可编程闸门
在 Claude Code 的工具执行链中,PreToolUse 是一个极具威力的 Hook。它在模型决定调用工具之后、实际执行该工具之前触发,为开发者提供了一个可以动态审计、修改甚至拦截工具调用的最后机会。
从定义开始
PreToolUse 本质上是 工具执行的同步守卫(Synchronous Guard)。
它不是一个事后的日志记录器,而是一个同步阻断点。它允许你根据工具的名称、输入参数或当前上下文,实时决定是否允许这次调用。你可以把它想象成一个自定义的防火墙规则:在流量(工具请求)到达目的地(工具实现)之前,先过一遍你的安全规则。
它也不是权限询问框(那由 PermissionRequest 负责),它是一个非交互式的逻辑判定点。
实现细节
该钩子的触发逻辑深度集成在 claude-code-opensource/src/services/tools/toolExecution.ts 中,其执行逻辑如下:
- 触发时机:模型发出
tool_use消息后,系统首先完成参数校验(Zod Validation)。校验通过后,立即调用runPreToolUseHooks派发事件。 - 上下文装配:系统为钩子准备了详尽的输入(
PreToolUseHookInput),包括:tool_name:即将被调用的工具名称。tool_input:经过校验的工具参数。tool_use_id:该次调用的唯一标识。
- 条件匹配(If Condition):利用
prepareIfConditionMatcher对工具名或特定参数模式进行前置筛选。例如,你可以定义一个只对Bash工具生效,且其命令中包含特定字符串的PreToolUse钩子。 - 裁决响应(Actionable Response): 钩子可以通过返回
HookJSONOutput实施以下干预:- 中断执行:通过返回
decision: "block"并提供理由,使该次工具调用失败(模型会看到报错并尝试纠正)。 - 参数改写(Updated Input):通过返回
updatedInput直接改写模型即将发送给工具的参数。这是目前唯一可以“无感”修正模型错误的手段。 - 权限覆盖:在特定逻辑下,Hook 可以强制授予或撤销该次调用的权限。
- 中断执行:通过返回
限制与陷阱
- 同步阻塞瓶颈:由于
PreToolUse是同步阻塞的,任何耗时的钩子操作(如网络请求或耗时的本地扫描)都会直接导致 Claude 响应变慢。 - 非 UI 交互性:它无法在工具执行前弹出一个自定义的询问对话框。它的裁决必须是非交互式的,依赖代码逻辑而非用户操作。
- 权限系统的优先级:
PreToolUse通常运行在基础权限校验(如规则匹配)之后。即使规则允许了,PreToolUse仍有权将其拦截。 - 仅限主进程工具:对于某些极其底层的内建指令,系统可能会绕过完整的钩子流程,这一点在实现时需进行特定验证。
延伸阅读
- 如果你想在工具执行后进行清理或结果校验,请看 PostToolUse:执行后的状态审查。
- 如果你想在系统弹出询问框前进行逻辑干预,请看 PermissionRequest:权限请求的自定义拦截。
- 如果你想了解 Hook 的基础调度机制,请看 Hook 系统架构解析。
源码锚点
claude-code-opensource/src/services/tools/toolExecution.ts— 工具执行的主循环及其对PreToolUse的触发。
📄 src/services/tools/toolExecution.ts — 工具执行的主循环及其对 `PreToolUse` 的触发。
typescript
runPreToolUseHooks,
} from './toolHooks.js'
/** Minimum total hook duration (ms) to show inline timing summary */
export const HOOK_TIMING_DISPLAY_THRESHOLD_MS = 500
/** Log a debug warning when hooks/permission-decision block for this long. Matches
* BashTool's PROGRESS_THRESHOLD_MS — the collapsed view feels stuck past this. */
const SLOW_PHASE_LOG_THRESHOLD_MS = 2000
/**
* Classify a tool execution error into a telemetry-safe string.
*
* In minified/external builds, `error.constructor.name` is mangled into
* short identifiers like "nJT" or "Chq" — useless for diagnostics.
* This function extracts structured, telemetry-safe information instead:
* - TelemetrySafeError: use its telemetryMessage (already vetted)
* - Node.js fs errors: log the error code (ENOENT, EACCES, etc.)
* - Known error types: use their unminified name
* - Fallback: "Error" (better than a mangled 3-char identifier)
*/
export function classifyToolError(error: unknown): string {claude-code-opensource/src/services/tools/toolHooks.ts—runPreToolUseHooks的具体实现及 Hook 决策的分发逻辑。
📄 src/services/tools/toolHooks.ts — `runPreToolUseHooks` 的具体实现及 Hook 决策的分发逻辑。
typescript
export async function* runPreToolUseHooks(
toolUseContext: ToolUseContext,
tool: Tool,
processedInput: Record<string, unknown>,
toolUseID: string,
messageId: string,
requestId: string | undefined,
mcpServerType: McpServerType,
mcpServerBaseUrl: string | undefined,
): AsyncGenerator<
| {
type: 'message'
message: MessageUpdateLazy<
AttachmentMessage | ProgressMessage<HookProgress>
>
}claude-code-opensource/src/types/hooks.ts—PreToolUseHookInput的字段定义。
📄 src/types/hooks.ts — `PreToolUseHookInput` 的字段定义。
typescript
export function isHookEvent(value: string): value is HookEvent {
return HOOK_EVENTS.includes(value as HookEvent)
}