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 1 或 return 1 在 bash 直覺對,在 hook 裡完全不是那個意思。
Exit code 對照表
這張表記下來,hook 八成的雷都在這。
| exit code | Claude 看到什麼 | 用途 |
|---|---|---|
| 0 | 沒事,繼續 | hook 通過,照常執行 tool |
| 2 | block,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 |
| 禁止跑某些 cmd | PreToolUse + 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 卡住時這三招都試:
-
Tee payload 到檔案——看 Claude 真的傳了什麼進來:
tee /tmp/hook-input.json | jq -r '.tool_input.file_path' -
/hooks看 trace——Claude Code 內建指令,列出本次 session 跑過哪些 hook、exit code、耗時。 -
echo 到 stderr——exit 2 的時候 stderr 會餵回 Claude,你可以順便確認文字長怎樣:
echo "DEBUG: matched pattern $pattern" >&2
進階雷:feedback loop
最容易被自己坑的場景:
- PreToolUse hook 偵測到不該做的事,exit 2
- stderr 寫了「不要這樣做,請改用 X」
- Claude 收到 feedback,改用 X 重試——但 X 也被同一條 hook 規則擋下
- Claude 再讀 stderr、再換做法、再被擋……
寫 block 訊息時要把「為什麼擋」「該怎麼做才會過」一起講清楚。模糊的「access denied」會讓 Claude 一直亂猜重試。
更糟的版本:在 PreToolUse 裡放 destructive command(例如刪檔、改 config),然後依賴 stderr 反饋——擋下去之後副作用已經發生,Claude 拿到的 feedback 又讓它繞回來再觸發一次。PreToolUse 應該只做檢查,不做動作。
接下來
下一篇看 Claude Code SDK——把 Claude Code 包成你自己應用裡的 agent。

