博客 • 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 怎样分支。

先记住这一点: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 settings:PreToolUse 条目 onFailure 为 block。
{
  "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。

hook 回调结果:hookName、onFailure、exitCode、timedOut。
{
  "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 日志模板。

hook 回调结果 JSON Schema:先过形状再进 hook_failure_gate。
{
  "$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 分支。

hook 成功:Bash tool 调用可继续。
{
  "tool": "Bash",
  "command": "npm test"
}
hook 失败 block:Bash 未执行。
{
  "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 枚举里。

审计日志:decision blocked、reason hook_callback_failed、hookName、exitCode。
{
  "tool": "Bash",
  "decision": "blocked",
  "reason": "hook_callback_failed",
  "hookName": "pre-bash.sh",
  "onFailure": "block",
  "exitCode": 2
}
日志或调用现象hook_failure_gate 返回运维动作
exitCode 0,timedOut false,onFailure blockallowtool 可执行;日志无需 hook_callback_failed。
exitCode 非零,onFailure block,timedOut falseblock对齐 decision blocked 与 reason hook_callback_failed 及 exitCode。
onFailure 未设 block 或取值 warnnot_block_mode不要写 hook_callback_failed;对照 fail-open 旧报表。
timedOut true 且 onFailure blocktimeout_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。

hook_failure_gate:schema、not_block_mode、allow、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. 1
    用 Schema 固定 hooks 形状

    打开 JSON Schema。第一栏贴 hooks settings schema,第二栏贴 PreToolUse 与 onFailure block 样本。required 或 enum 错误应先标红,hook_failure_gate 不应在形状错误时返回 allow 或 block。

  2. 2
    用格式化核对 tool 调用

    打开 JSON 格式化,贴 Bash tool 调用 JSON。确认 tool 与 command 键存在,再对照 hook_failure_gate 是否应走 block 或 allow。

  3. 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 返回值。

返回博客