一家很讲规矩的老字号面馆。墙上挂着一长溜菜牌,每道面旁边都钉着一张配方卡,卡上写明这道面「必须」用哪几样料——少一样,就不成其为这道面。
后厨有条铁律:厨师不许凭印象下锅。客人点单时,他得先照菜名在菜牌上找到那道面,再把单子上写的料一项项对着配方卡核——配方卡要求的料,单子上必须样样齐、样样对;但凡缺一样、或写了张冠李戴的料,这张单子当场退回,锅碗一概不碰。只有逐项核对全部通过,他才真正起锅下料。
这天传菜的小工递进来一张单子。厨师接过来一看,配方卡头一项必填的主料那一栏是空的。不是写错字,是压根没填——一片空白。
厨师没有犹豫。空白对不上配方卡的必填项,这张单子不合规。他也没有「凭印象补一个主料」凑合下锅——那恰恰是铁律最忌讳的。于是他把单子退回前台,顺带讲清楚为什么退:第几项主料没填,请补齐了再来。锅,自始至终没动。
让这家店几十年不出岔子的,从来不是厨师手快,而是那条「先照配方卡逐项核对、核不过就退单、核过了才动锅」的规矩。一张缺了必填项的单子,正好撞在这条规矩的第一道关上——在它能把后厨搅乱之前,就被挡了回去。
先猜揭晓前,先押一个猜测 · 你觉得这讲的是?
谜底 · The Concept
Anatomy of a tool — the input-schema boundary (validate before run)
Tools · 工具系统
中文速览 · Quick read
A tool in Claude Code is not just a function — it is a typed contract. The Tool type (claude-code/src/Tool.ts:362) bundles a human name, an async call implementation, a description, and — the part this page is about — a readonly inputSchema (claude-code/src/Tool.ts:394): a Zod schema declaring exactly which arguments the tool requires and of what type. Before that call body is ever entered, the harness checks the model's proposed arguments against this schema at a single chokepoint. A tool-use whose required field is missing — arriving as undefined — is rejected at that boundary, and the tool body never runs. This page reads that validate-before-run boundary: the menu's recipe card (inputSchema) and the iron rule that the ticket is matched against it before the kitchen moves.
When the model emits a tool_use, the harness routes it through checkPermissionsAndCallTool (claude-code/src/services/tools/toolExecution.ts:599). Its very first act, before anything else, is to parse the raw input against the tool's schema: const parsedInput = tool.inputSchema.safeParse(input) (claude-code/src/services/tools/toolExecution.ts:615) — the source even annotates why, noting that "the model is not great at generating valid input." If the parse fails (!parsedInput.success), the harness does not call the tool; it formats the Zod error and hands a tool_result back to the model carrying <tool_use_error>InputValidationError: …</tool_use_error> (claude-code/src/services/tools/toolExecution.ts:670). Only on success does control proceed to the tool's own validateInput (claude-code/src/Tool.ts:489, invoked at toolExecution.ts:683), then to checkPermissions — which by contract "is only called after validateInput() passes" (claude-code/src/Tool.ts:500) — and finally to const result = await tool.call(...) (claude-code/src/services/tools/toolExecution.ts:1207). A missing required field therefore cannot reach tool.call; it is stopped at step one with a precise, model-readable error naming exactly which field failed.
This boundary is what makes a tool safe to wire to real side effects. tool.call runs file edits, shell commands, network requests; letting an undefined required argument flow into it would mean the failure surfaces deep inside the tool body — a crash far from its cause — or, worse, a silently malformed operation that "succeeds" on garbage. By parsing against inputSchema first (claude-code/src/services/tools/toolExecution.ts:615) and converting any mismatch into an InputValidationError returned to the model (claude-code/src/services/tools/toolExecution.ts:670), the harness turns "an absent value propagating silently until it detonates somewhere downstream" into "one loud, located rejection at the door" — and because the error names the offending field, the model can repair the call and retry. The layered ordering — schema parse → validateInput → checkPermissions → call (claude-code/src/Tool.ts:489, claude-code/src/Tool.ts:500, claude-code/src/services/tools/toolExecution.ts:1207) — means each guard runs only once the cheaper, earlier guard has already passed.
- The
Toolcontract —name,inputSchema,validateInput,call: claude-code/src/Tool.ts:362, claude-code/src/Tool.ts:394, claude-code/src/Tool.ts:489. - The validate-before-run chokepoint —
inputSchema.safeParseand theInputValidationErrorearly return: claude-code/src/services/tools/toolExecution.ts:615, claude-code/src/services/tools/toolExecution.ts:670. - The ordering guarantee —
checkPermissionsonly aftervalidateInput, thentool.call: claude-code/src/Tool.ts:500, claude-code/src/services/tools/toolExecution.ts:1207.
classDiagram
class Tool {
<<interface>>
+name string
+description string
+inputSchema ZodSchema
+validateInput(input, ctx)
+checkPermissions(input, ctx)
+call(input, ctx) AsyncGenerator
}
class checkPermissionsAndCallTool {
+safeParse(input)
+runPipeline()
}
class InputValidationError {
+returnedToModel bool
}
checkPermissionsAndCallTool ..> Tool : inputSchema.safeParse first
checkPermissionsAndCallTool ..> InputValidationError : on parse fail
checkPermissionsAndCallTool ..> Tool : then validate, permit, call 读法:Tool 是一份带 inputSchema 的类型契约(Tool.ts:362,394)。
执行的唯一入口 checkPermissionsAndCallTool 先做 inputSchema.safeParse(input)
(toolExecution.ts:615);失败即返回 InputValidationError 给模型、不进 call
(:670)。只有通过才依次 validateInput → checkPermissions → call
(Tool.ts:489,500,toolExecution.ts:1207)——validate-before-run。
模型发来一次 tool_use,但必填参数没填——它到达后厨时是
undefined。在真正执行(tool.call)之前,harness 先把这次调用拿去和
工具的 inputSchema 逐项核对(safeParse)。给必填字段
file_path 填个真值,或留它缺省;再决定门口要不要装上这道
schema 护栏,然后单步或整跑,看这次调用怎么走。
FileEditToolundefined"…" / "…"Set file_path, choose the guard, then run.
undefined 不是异常,而是「没有值」。真实的 harness 在 checkPermissionsAndCallTool
里第一步就 tool.inputSchema.safeParse(input):必填项缺失 → !success →
直接把 InputValidationError 回给模型,tool.call 根本不会执行
(toolExecution.ts:615 / 670)。这道门口的 schema 护栏,把「undefined 一路静默传播、最终在
工具体内某处炸开」换成「在门口一次响亮、就地的退回」:病因 == 症状,blast radius == 0。
What it is
A tool in Claude Code is not just a function — it is a typed contract. The Tool type (claude-code/src/Tool.ts:362) bundles a human name, an async call implementation, a description, and — the part this page is about — a readonly inputSchema (claude-code/src/Tool.ts:394): a Zod schema declaring exactly which arguments the tool requires and of what type. Before that call body is ever entered, the harness checks the model's proposed arguments against this schema at a single chokepoint. A tool-use whose required field is missing — arriving as undefined — is rejected at that boundary, and the tool body never runs. This page reads that validate-before-run boundary: the menu's recipe card (inputSchema) and the iron rule that the ticket is matched against it before the kitchen moves.
How it works
When the model emits a tool_use, the harness routes it through checkPermissionsAndCallTool (claude-code/src/services/tools/toolExecution.ts:599). Its very first act, before anything else, is to parse the raw input against the tool's schema: const parsedInput = tool.inputSchema.safeParse(input) (claude-code/src/services/tools/toolExecution.ts:615) — the source even annotates why, noting that "the model is not great at generating valid input." If the parse fails (!parsedInput.success), the harness does not call the tool; it formats the Zod error and hands a tool_result back to the model carrying <tool_use_error>InputValidationError: …</tool_use_error> (claude-code/src/services/tools/toolExecution.ts:670). Only on success does control proceed to the tool's own validateInput (claude-code/src/Tool.ts:489, invoked at toolExecution.ts:683), then to checkPermissions — which by contract "is only called after validateInput() passes" (claude-code/src/Tool.ts:500) — and finally to const result = await tool.call(...) (claude-code/src/services/tools/toolExecution.ts:1207). A missing required field therefore cannot reach tool.call; it is stopped at step one with a precise, model-readable error naming exactly which field failed.
Why it matters
This boundary is what makes a tool safe to wire to real side effects. tool.call runs file edits, shell commands, network requests; letting an undefined required argument flow into it would mean the failure surfaces deep inside the tool body — a crash far from its cause — or, worse, a silently malformed operation that "succeeds" on garbage. By parsing against inputSchema first (claude-code/src/services/tools/toolExecution.ts:615) and converting any mismatch into an InputValidationError returned to the model (claude-code/src/services/tools/toolExecution.ts:670), the harness turns "an absent value propagating silently until it detonates somewhere downstream" into "one loud, located rejection at the door" — and because the error names the offending field, the model can repair the call and retry. The layered ordering — schema parse → validateInput → checkPermissions → call (claude-code/src/Tool.ts:489, claude-code/src/Tool.ts:500, claude-code/src/services/tools/toolExecution.ts:1207) — means each guard runs only once the cheaper, earlier guard has already passed.
Read the source
- The
Toolcontract —name,inputSchema,validateInput,call: claude-code/src/Tool.ts:362, claude-code/src/Tool.ts:394, claude-code/src/Tool.ts:489. - The validate-before-run chokepoint —
inputSchema.safeParseand theInputValidationErrorearly return: claude-code/src/services/tools/toolExecution.ts:615, claude-code/src/services/tools/toolExecution.ts:670. - The ordering guarantee —
checkPermissionsonly aftervalidateInput, thentool.call: claude-code/src/Tool.ts:500, claude-code/src/services/tools/toolExecution.ts:1207.
来源 · Source citations
- [1]
claude-code/src/Tool.ts:362 - [2]
claude-code/src/Tool.ts:394 - [3]
claude-code/src/Tool.ts:489 - [4]
claude-code/src/Tool.ts:500 - [5]
claude-code/src/services/tools/toolExecution.ts:599 - [6]
claude-code/src/services/tools/toolExecution.ts:615 - [7]
claude-code/src/services/tools/toolExecution.ts:670 - [8]
claude-code/src/services/tools/toolExecution.ts:1207