博客 • Claude Code / Agent SDK
升级到 Claude Code 2.1.317 之后,Agent SDK hook 设置 onFailure 为 block 时,回调失败要在 JSON 日志里怎样记下拦截?
2026 年 11 月 3 日,Anthropic 发布 Claude Code 2.1.317。Agent SDK hook 回调配置新增 onFailure 取值 block:当 PreToolUse 等 hook 脚本非零退出或超时时,客户端不再 fail-open 继续执行 tool,而是拦截该次 tool 动作并在审计 JSON 里记下 blocked。
hook 回调结果 JSON 应写出 hookName、onFailure block、exitCode 与 timedOut;tool 调用被拦后的审计 JSON 应写 decision blocked、reason hook_callback_failed,并保留 hookName 与 exitCode。运维可用本地 hook_failure_gate 对照 schema、not_block_mode、allow、block、timeout_block 五条分支,而不是把回调失败当成 permission policy deny。
allow 与 deny 队列写法见 Agent permission policy;Managed Agents 的 auto 评测上下文见 Managed Agents auto 评测;会话级工作流 JSON 见 Claude Code workflow。本篇只写 2.1.317 的 onFailure block、hooks settings 与回调结果 JSON、Bash 调用样本、hook_callback_failed 审计字段,以及 hook_failure_gate 怎样分支。
- hooks settings 里 PreToolUse 等条目必须声明 onFailure 为 block 才会在回调失败时拦截 tool;未声明或取值不是 block 时 hook_failure_gate 应返回 not_block_mode,不要写 reason hook_callback_failed。
- hook 回调结果 JSON 必须带 hookName、onFailure、整数 exitCode 与布尔 timedOut;exitCode 为 0 且 timedOut 为 false 时返回 allow,非零 exitCode 且 onFailure block 时返回 block。
- 审计日志在 block 分支应写 decision blocked、reason hook_callback_failed,并保留 hookName 与 exitCode;SIEM 应按 hookName 聚合而不是混用 permission policy 的 deny reason。
- timedOut 为 true 且 onFailure block 时 hook_failure_gate 返回 timeout_block;settings 或回调结果 JSON 形状错误时先返回 schema,再进入 block 判定。
先记住这一点:2.1.317 改的是 hook 回调失败时的 block 语义与 hook_callback_failed 审计字段,不是重写 permission policy 里的 allow 或 deny 表。下面按 changelog、hooks settings JSON、回调结果 JSON、Bash 调用样本、审计日志与对照表、hook_failure_gate 源码,以及 JSONNote 里怎样校验形状。
2.1.317 怎样让 hook 回调失败时 block 而不是 fail-open?
变更说明见 Claude Code changelog,标签页见 v2.1.317 release。相对更早 Agent SDK hook 行为,最显眼的是 onFailure block:PreToolUse 等 hook 脚本非零退出或超时时,tool 动作被拦截,而不是记录警告后继续执行。
拦截发生在 hook 回调完成之后、tool 执行之前。审计 JSON 应保留 decision blocked、reason hook_callback_failed、hookName 与 exitCode,方便 SIEM 按失败 hook 归因,而不是只记一条泛化的 tool denied。
PreToolUse 与 onFailure block 在 hooks settings JSON 里怎样写?
在用户或项目 settings 的 hooks 对象里为 PreToolUse 配置 matcher 与 command,并在同一条目设置 onFailure 为 block。2.1.317 起只有显式 block 才会在回调失败时拦截 tool;缺省或其它取值仍走 fail-open 或 warn 路径,hook_failure_gate 应返回 not_block_mode。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "./hooks/pre-bash.sh",
"onFailure": "block"
}
]
}
]
}
}
若运维把 onFailure 写成 Block 大小写混用或漏掉 hooks 层级,本地 schema 校验应先失败并返回 schema,再进入 hook_failure_gate 的 block 分支。不要把形状错误当成 not_block_mode。
hook 回调结果 JSON 里 hookName 与 exitCode 怎样写?
客户端在 hook 回调完成后写入结果 JSON:hookName 标识触发的 PreToolUse 条目,onFailure 回显 settings 里的 block,exitCode 为脚本进程退出码,timedOut 表示是否因超时被杀。permission 仍 allow 的 Bash 调用也可能因 hook 失败而被 block。
{
"hookName": "pre-bash.sh",
"onFailure": "block",
"exitCode": 2,
"timedOut": false
}
exitCode 必须为整数,timedOut 必须为布尔;缺 hookName 或 onFailure 形状错误时 hook_failure_gate 先返回 schema。onFailure 不是 block 时返回 not_block_mode,避免误用 hook_callback_failed 日志模板。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["hookName", "onFailure", "exitCode", "timedOut"],
"properties": {
"hookName": { "type": "string", "minLength": 1 },
"onFailure": { "type": "string", "enum": ["block", "continue"] },
"exitCode": { "type": "integer" },
"timedOut": { "type": "boolean" }
}
}
Bash tool 调用 JSON 在 hook 拦截前后应怎样对照?
样本:permission policy 已对 Bash allow,tool 调用 JSON 含 tool Bash 与 command npm test。PreToolUse hook 脚本 exitCode 2 且 onFailure block 时客户端不执行 Bash,审计 JSON 写 decision blocked 与 reason hook_callback_failed。对照时应保留原始 command 与 hookName,便于 hook_failure_gate 走 block 分支。
{
"tool": "Bash",
"command": "npm test"
}
{
"hookName": "pre-bash.sh",
"onFailure": "continue",
"exitCode": 2,
"timedOut": false
}
审计日志里 decision blocked 与 hook_callback_failed 应怎样写?
hook 回调失败且 onFailure block 时,tool 审计 JSON 应写 decision blocked、reason hook_callback_failed,并保留 hookName 与非零 exitCode;timedOut 为 true 时可额外标记 timeout 字段但 reason 仍用 hook_callback_failed。不要把 permission deny 与 hook block 混在同一 reason 枚举里。
{
"tool": "Bash",
"decision": "blocked",
"reason": "hook_callback_failed",
"hookName": "pre-bash.sh",
"onFailure": "block",
"exitCode": 2
}
| 日志或调用现象 | hook_failure_gate 返回 | 运维动作 |
|---|---|---|
| exitCode 0,timedOut false,onFailure block | allow | tool 可执行;日志无需 hook_callback_failed。 |
| exitCode 非零,onFailure block,timedOut false | block | 对齐 decision blocked 与 reason hook_callback_failed 及 exitCode。 |
| onFailure 未设 block 或取值 warn | not_block_mode | 不要写 hook_callback_failed;对照 fail-open 旧报表。 |
| timedOut true 且 onFailure block | timeout_block | 日志仍用 hook_callback_failed;SIEM 单独计数 timeout_block。 |
hook_failure_gate 怎样在 schema 与五条业务分支间选择?
实现顺序:先用 hooks settings 与回调结果 JSON Schema 校验 onFailure 与 hookName、exitCode、timedOut;onFailure 不是 block 返回 not_block_mode;timedOut true 且 block 返回 timeout_block;exitCode 0 返回 allow;非零 exitCode 且 block 返回 block;形状错误返回 schema。
不要把 block 与 timeout_block 合成单一 hook_failed 位。报表分开计数,才能区分 not_block_mode fail-open、exitCode 失败 block、以及真正的 timeout_block。
def actual_type(data: object) -> str:
if data is None:
return "null"
if isinstance(data, bool):
return "boolean"
if isinstance(data, list):
return "array"
if isinstance(data, int):
return "integer"
if isinstance(data, float):
return "number"
if isinstance(data, str):
return "string"
if isinstance(data, dict):
return "object"
raise TypeError(type(data))
def validate(data: object, schema: dict) -> list[str]:
errors: list[str] = []
if not isinstance(schema, dict):
return errors
types = schema.get("type")
if isinstance(types, str):
types = [types]
actual = actual_type(data)
if types and not any(t == actual or (t == "number" and actual == "integer") for t in types):
errors.append("type")
if "enum" in schema and not any(value == data for value in schema["enum"]):
errors.append("enum")
if isinstance(data, str) and "minLength" in schema and len(data) < schema["minLength"]:
errors.append("minLength")
if isinstance(data, dict) and schema.get("properties"):
for key in schema.get("required") or []:
if key not in data:
errors.append(f"required:{key}")
if schema.get("additionalProperties") is False:
for key in data:
if key not in schema["properties"]:
errors.append(f"additional:{key}")
for key, sub in schema["properties"].items():
if key in data and data[key] is not None:
errors.extend(validate(data[key], sub))
return errors
def hook_failure_gate(ctx: dict, schema: dict) -> str:
if validate(ctx, schema):
return "schema"
mode = ctx.get("onFailure")
if mode != "block":
return "not_block_mode"
if ctx.get("timedOut"):
return "timeout_block"
code = ctx.get("exitCode")
if code == 0:
return "allow"
return "block"
在 JSONNote 里怎样校验 hooks settings、回调结果与审计日志?
- 1用 Schema 固定 hooks 形状
打开 JSON Schema。第一栏贴 hooks settings schema,第二栏贴 PreToolUse 与 onFailure block 样本。required 或 enum 错误应先标红,hook_failure_gate 不应在形状错误时返回 allow 或 block。
- 2用格式化核对 tool 调用
打开 JSON 格式化,贴 Bash tool 调用 JSON。确认 tool 与 command 键存在,再对照 hook_failure_gate 是否应走 block 或 allow。
- 3用 Diff 对比 allow 与 blocked 审计日志
打开 JSON Diff。左边放 hook 成功 allow 样本,右边放 decision blocked 与 reason hook_callback_failed 样本。Diff 高亮的 hookName 就是 SIEM 规则最该锁定的一行。
常见问题
onFailure block 与旧版 fail-open 有何不同?
旧版 hook 回调失败时通常记录 stderr 或警告后继续执行 tool。2.1.317 在 settings 里显式 onFailure block 时,非零 exitCode 或 timeout 会写 decision blocked 与 reason hook_callback_failed 并跳过 tool。未设 block 时仍 fail-open,hook_failure_gate 应返回 not_block_mode。
exitCode 2 与其它非零码在日志里怎样区分?
2.1.317 起 block 语义看 exitCode 是否为零,而不是只认 2。审计 JSON 应原样写整数 exitCode,SIEM 可单独统计 2 与 1 或 127,但 hook_failure_gate 的 block 分支对所有非零 exitCode 一致。不要把 exitCode 省略成布尔 failed。
onFailure 除 block 外还有哪些取值?
常见还有 warn 或缺省 fail-open:回调失败只告警仍执行 tool。只有 block 会触发 decision blocked。运维在报表里用 not_block_mode 计数 fail-open,不要把 warn 误记为 hook_callback_failed。
hook block 与 permission policy 层是什么关系?
permission policy 决定 tool 类型默认 allow、deny 或 ask;hook 在 tool 执行前再跑一层脚本校验。即使 policy 已 allow Bash,PreToolUse 失败且 onFailure block 仍会拦截。两套 JSON 分开保存,hook_failure_gate 只镜像 hook 层,不替 policy 做 allow 判定。
本篇是否提供攻击或绕过教程?
不是。本文只解释 2.1.317 onFailure block 的 JSON 字段形状、hook_callback_failed 审计写法与 hook_failure_gate 对照分支,帮助运维与合规做校验和 SIEM 归因。不提供如何利用 hook 失败或 exitCode 绕过管控的步骤。
总结
Claude Code 2.1.317 在 Agent SDK hook 上支持 onFailure block,回调失败时在审计 JSON 里写 decision blocked、reason hook_callback_failed、hookName 与 exitCode。运维应用 hooks settings、回调结果、Bash 调用与 blocked 审计四套样本驱动 hook_failure_gate,而不是只盯 permission allow。
直接结论:settings 或回调 JSON 不过 schema 返回 schema;onFailure 不是 block 返回 not_block_mode;exitCode 0 返回 allow;非零 exitCode 且 block 返回 block;timedOut true 且 block 返回 timeout_block;block 与 timeout_block 的审计对齐 decision blocked、reason hook_callback_failed、hookName 与 exitCode。
下一步:用 Schema 固定 hooks settings 与回调结果,用格式化看清 Bash 调用,用 Diff 对比 allow 与 blocked 审计日志,并在报表里分开统计 hook_failure_gate 返回值。