tool_decision 事件裡的 source: "config",其實什麼都沒回答。

它的意思是「這個決定來自設定」,而官方文件把「設定」圈得很大:專案設定、你個人設定裡的 allow/deny、managed policy、--allowedTools/--disallowedTools、當下的 permission mode、auto mode 的 classifier、同一個 session 先前給過的授權、工具自己的安全檢查,全部算 "config"。文件自己也承認這件事,原文是「The source value doesn’t indicate which of these sources matched.」

這篇先交代邊界:我只讀了 monitoring-usage 文件和 changelog,沒有實際接 collector 跑過,下面每個欄位名和值都是照文件抄的,樣子對不對要你自己接起來看。

一本帳只記放不放行

把 tool_decision 想成大樓門口的出入登記簿。它記得很清楚:哪個工具、放行還是拒絕。可是簿子上「拒絕」旁邊的原因欄,多半只寫「依規定」。你想知道到底是保全不讓進、門禁卡過期,還是電梯本身鎖了,這本簿子答不出來。

另一本簿子是 2.1.307 才補上的。changelog 在 2026 年 10 月 21 日那版寫:新增 claude_code.safety_event,針對每一個被 permission rule、auto mode、safety check、hook 或 sandbox 擋下、詢問或退回的工具呼叫。文件對它的描述是:檢查拒絕了呼叫、或要求出現權限提示時就記一筆;sandbox 的網路 proxy 拒絕連線、hook 擋下結果時也記。

兩本簿子靠同一個鑰匙對起來。文件的做法寫得很直白:要查是誰擋的,去找跟它同一個 tool_use_id 的 safety event,讀 category 和 rule_source。

文件裡 managed settings 的 deny 規則拒絕 Bash 的官方範例,屬性長這樣(我把它排成一列一個,原文不是這個排版):

1
2
3
4
5
6
7
8
9
10
11
category: "managed_policy"
decision: "reject"
decision_source: "config"
reason_code: "rule:policySettings"
rule_source: "policySettings"
tool_class: "bash"
tool_name: "Bash"
tool_source: "builtin"
permission_mode: "default"
sandbox_violation_count: "0"
sandbox_enabled: "false"

decision_source: "config" 又出現了。可是這次旁邊有 category 和 rule_source,原因欄不再是空話。

最讓我意外的一格:被拒了,卻沒有這本簿子

照前面的說法,很容易推出「只要有拒絕就一定有 safety event,反查流程不會斷」。文件把這個推論打破了。

有一類拒絕不會留下 safety event:沒有 category 的提示,而且沒人能回答它。文件舉的場景是 -p 執行,既沒給 --permission-prompt-tool,也沒給 --permission-prompts none。提示跳出來,沒有人在,於是被拒。「沒有 category」在文件裡的定義是:沒有任何 category 值能叫出這項檢查的名字,例子是 default 模式下,對工作目錄內檔案的一次編輯提示。

那怎麼找?文件給的是一個反向查法:找 tool_decision 事件,reject,source 是 "config",而且沒有任何 safety event 跟它共用 tool_use_id。查「有」查不到的東西,改成查「該有卻沒有」。

另外,文件還列了幾種同樣沒有 safety event 的情形:沒有 category 的提示被核准、AskUserQuestion 與 ExitPlanMode 的呼叫、canUseTool 或 --permission-prompt-tool 核准的提示、只有 PreToolUse hook 要求、而且被核准的提示(被拒絕的那一種,文件歸在 user_rejected,有 safety event)。例外是 sandbox_network、sandbox_other、hook_review 這幾類,它們照樣會有。

對長時間無人看管跑 -p 的人,這一格最有用。任務悄悄少做了一步,日誌裡看起來只是 reject,證據恰恰在「對不上的那一筆」。

這一段的查法我沒有實際跑過。文件說「無人回答的提示被拒」發生在 -p,沒有指定上述兩個選項的情形;--permission-prompts none 旗標本身的完整定義,我沒讀 CLI 文件,不敢多說。

另外兩個容易讀歪的欄位

tool_decision 在 2.1.301 多了 classifier_verdict、permission_mode,後者的值文件逐項列了:"default"、"plan"、"acceptEdits"、"bypassPermissions"、"dontAsk"、"auto"。手動模式會顯示成 "default";subagent 的呼叫回報的是 subagent 自己的 permission mode,不是 session 的。

classifier_verdict 有四種值:"allow"、"block"、"no_verdict"、"unavailable"。後兩個最容易讀錯。"no_verdict" 是檢查跑了但沒有可用結果,Claude Code 因此拒絕這次呼叫;文件特別補一句,這個拒絕不代表 classifier 判定呼叫不安全。"unavailable" 是請求 classifier 本身失敗,例如 rate limit、timeout、連線錯誤,同樣是 Claude Code 自己決定拒絕。你若拿 reject 的筆數當「classifier 擋下多少危險動作」,這兩種會把數字灌大。

還有 classifier_category,要開 OTEL_LOG_TOOL_DETAILS=1 才有,而且只在三個條件同時成立時出現:verdict 是 "block"、classifier 只點名了一條規則、那條規則是內建的,不是你寫的。所以你自己寫的 autoMode 規則擋下來時,這個欄位不會出現。自訂規則該放哪一層的取捨,我在 10 月 13 日寫過,見那篇 autoMode 的文章;這一類規則擋下來時,事件上不會帶規則名字,這點要先有心理準備。

事件本身也不帶細節。文件說 safety event 自己的屬性裡沒有指令文字、檔案路徑、主機名,也沒有 MCP server 與工具名(原文是 “MCP server and tool names”)。這句和它自己的屬性清單有一點對不上:清單裡明明列了 tool_name。兩者怎麼並存,文件沒交代,我沒有實跑,只能照原文標出來。想看到被擋的是哪條指令,要設 OTEL_LOG_TOOL_DETAILS=1,再去讀對應 tool_decision 事件上的 tool_parameters。這個設定會把 Bash 指令、MCP 名稱、工具輸入都記下來,預設是關的,要不要開是個得先問資安的決定。啟用 logs 匯出本身的最小設定,文件的 Quick start 是 CLAUDE_CODE_ENABLE_TELEMETRY=1、OTEL_LOGS_EXPORTER=otlp、OTEL_EXPORTER_OTLP_PROTOCOL=grpc、OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317;文件註明 Claude Code 沒有預設 protocol,要自己設。匯出間隔預設 5000 毫秒。

至於 category,文件列出了 14 個值,我在這裡不逐個抄,因為抄一遍沒有比讀原表多告訴你什麼。幾個我會先去看的:managed_policy(managed settings 的 deny/ask)、permission_rule(其他設定的 deny/ask)、hook、auto_mode_block、safety_check、static_analysis(Bash 或 Monitor 的指令寫法有問題,文件舉的例子是太複雜無法分析)、sandbox_network(呼叫已經執行,但 sandbox 的網路 proxy 拒絕了連線)。reason_code、rule_source 這幾個欄位,文件寫的是「such as」,是舉例,完整值我不知道。想篩 proxy 拒絕,文件的做法是找 sandbox_violation_count 存在而且不是 "0" 的事件,跨 category 都能用。

我的立場,和會讓我改口的條件

我的立場是:儀表板上要看「誰擋的」,不要拿 tool_decision.source 分組,一律用 safety event 的 category 分;source 只拿來看放行或拒絕的總數。理由很單純,"config" 這個桶太大,分出來的圖大半都叫 config。

這個立場有前提,也有會推翻它的條件。前提是你真的在意「為什麼擋」;如果你只想知道今天被拒了幾次,一本簿子就夠,不必多接一條事件。至於改口的條件有兩個:實際接上 collector 之後,發現 tool_use_id 對不上,或同一筆呼叫配出多筆 safety event,那反查流程就得重想,我現在只能說文件這樣寫;另一個是之後文件把 source 拆細,到 "config" 底下能直接分辨層級,那 join 這一步就沒必要了。

這篇沒有踩坑經過可寫,因為我沒動手跑。7 月 28 日那篇接 OpenTelemetry 的文章管的是花了多少錢,這篇管的是一次呼叫為什麼被拒。事件清單我也只擷取到文件前段,確定有 tool_decision 和 safety_event,其餘事件的完整清單沒有讀完,不寫「共有幾種」。

一個決定,兩本帳:一本記結果,一本記原因。哪天你發現有結果、沒原因,那一筆本身就是線索。

原文來源:Monitoring - Claude Code Docs、Changelog