博客 • 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 返回值。

返回博客