Applied AI School
v0 · 規劃中
Anthropic

Hooks 雷區

exit code 語意、PostToolUse 不能 block、debug 技巧。

TL;DR

  • exit code 語意要記清:0 = pass / 2 = block + stderr 給 Claude / 其他 non-zero = error log
  • PostToolUse 不能 block——tool call 已經發生,exit 2 只是事後抗議
  • Hook 是同步的,每個 tool call 都會等它跑完——慢的 hook 會拖死整個 session

一個情境:block 不掉的 .env

你寫了一條 PreToolUse hook,想 block 掉 Claude 對 .env 的讀取——確保密鑰不會被讀進 context。寫完測試,發現有時候有效、有時候 Claude 還是大方地把整個檔案讀出來。

九成的機率:exit code 寫錯了

Hook 的「成功 / 失敗 / 阻擋」是用 process exit code 表達的,不是 stdout 裡的訊息。寫 exit 1return 1 在 bash 直覺對,在 hook 裡完全不是那個意思。

Exit code 對照表

這張表記下來,hook 八成的雷都在這。

exit codeClaude 看到什麼用途
0沒事,繼續hook 通過,照常執行 tool
2block,stderr 內容當 feedback 餵回 Claude拒絕這次 tool call,並告訴 Claude 為什麼
其他 non-zero(1、127…)寫進 error log,但 tool 照常執行hook 自己壞了,不影響 Claude

最常踩的雷:想 block 但寫了 exit 1——Claude 完全不會理你,照樣讀檔。只有 exit 2 才是 block。

PostToolUse 不能 block

新手常見想法:「我在 PostToolUse 檢查結果,不對就 block 掉。」

不行。 語意上 too late——tool call 已經跑完,副作用(檔案被改、API 被呼叫、檔案被讀進 context)已經發生。你 exit 2 只是事後對著空氣抗議。

想 block,規則必須搬到 PreToolUse

想做的事該用哪個 hook
禁止讀某些檔PreToolUse + exit 2
禁止跑某些 cmdPreToolUse + exit 2
Lint / format 改完的檔PostToolUse(block 沒意義,讓它跑就好)
觀察、記 log兩個都行,看時機

Stdin / stdout 怎麼吃 payload

Hook 從 stdin 拿到 JSON payload,裡面有 tool 名、參數、session info。bash 用 jq 拆最方便:

#!/bin/bash
# PreToolUse hook:擋 .env
file=$(jq -r '.tool_input.file_path')

if [[ "$file" == *.env* ]]; then
  echo "Reading .env is blocked by policy" >&2
  exit 2
fi
exit 0

重點:

  • 訊息要寫到 stderr>&2),不是 stdout——只有 stderr 在 exit 2 時會被當 feedback 給 Claude
  • exit 0 / exit 2 是給 Claude 的訊號,stdout 內容對 Claude 沒意義

Debug 三招

寫 hook 卡住時這三招都試:

  1. Tee payload 到檔案——看 Claude 真的傳了什麼進來:

    tee /tmp/hook-input.json | jq -r '.tool_input.file_path'
  2. /hooks 看 trace——Claude Code 內建指令,列出本次 session 跑過哪些 hook、exit code、耗時。

  3. echo 到 stderr——exit 2 的時候 stderr 會餵回 Claude,你可以順便確認文字長怎樣:

    echo "DEBUG: matched pattern $pattern" >&2

進階雷:feedback loop

最容易被自己坑的場景:

  1. PreToolUse hook 偵測到不該做的事,exit 2
  2. stderr 寫了「不要這樣做,請改用 X」
  3. Claude 收到 feedback,改用 X 重試——但 X 也被同一條 hook 規則擋下
  4. Claude 再讀 stderr、再換做法、再被擋……

寫 block 訊息時要把「為什麼擋」「該怎麼做才會過」一起講清楚。模糊的「access denied」會讓 Claude 一直亂猜重試。

更糟的版本:在 PreToolUse 裡放 destructive command(例如刪檔、改 config),然後依賴 stderr 反饋——擋下去之後副作用已經發生,Claude 拿到的 feedback 又讓它繞回來再觸發一次。PreToolUse 應該只做檢查,不做動作。

接下來

下一篇看 Claude Code SDK——把 Claude Code 包成你自己應用裡的 agent。