Claude Code Hooks 設定、事件與 exit code 2
了解 Claude Code Hooks 的設定位置與觸發事件,以及 exit code 2 如何在工具呼叫前取消操作,並查看 stderr 回傳原因與安全性代價。
Claude Code hook 是什麼
Claude Code hook 是 Claude Code 在自身生命週期的固定時間點,自動執行的 shell 命令。這就是 hook 與規則檔案的全部差異。CLAUDE.md 中的指示是建議,模型會根據其上下文中的其他內容權衡這些指示。hook 是程式碼,無論模型是否同意,都會執行。如果您的 agent 一再略過您已經提醒兩次的 formatter,就不需要更強硬的指示。您需要的是 hook。
這項機制很精簡。您可以在設定檔中,使用事件名稱註冊命令。事件觸發時,Claude Code 會執行您的命令,並以 JSON(JavaScript object notation)格式將事件資料寫入其標準輸入(stdin)。您的命令會讀取這些資料、執行工作,並以結束狀態回應。PreToolUse hook 回傳 exit 2 時,會在工具呼叫執行前取消該呼叫;而您的指令碼寫入標準錯誤(stderr)的內容,會以原因的形式傳回模型。
本節的事件名稱與欄位名稱來自 Claude Code hooks 參考資料,並於 August 2026 針對 release 2.1.232 進行確認。這項介面變動快速,因此在複製任何部落格文章(包括本文)的 JSON 之前,請先查閱您所使用版本的參考資料。使用 claude --version 顯示您目前的版本。
Hook 設定的位置
Hook 是設定檔中的 JSON 區塊。共有 6 個位置可以放置 Hook,而設定檔的適用範圍就是 Hook 的適用範圍。
~/.claude/settings.json:套用至電腦上的所有專案,但不會套用至其他人的電腦。.claude/settings.json:套用至單一專案,並提交至 repository,因此所有 clone 該專案的人都會取得此 Hook。.claude/settings.local.json:套用至單一專案,但僅限於你的電腦。- Managed policy settings:套用至整個組織,由管理員設定。
- Plugin 內的
hooks/hooks.json:在該 Plugin 啟用期間有效。 - Skill 或 subagent 的 frontmatter:在該元件啟用期間有效。
來自這些檔案的 Hook 設定會合併,而不是彼此覆寫。專案設定檔會將其中的 Hook 加入 user settings 中的 Hook,而不是取代它們。因此,同一個事件可以包含來自多個檔案的多個 Hook。設定 "disableAllHooks": true 會停用這些 Hook,但有一個例外:來自 managed policy settings 的 Hook 仍會執行,除非同一設定也套用至 managed settings。
在工作階段中執行 /hooks,即可列出目前已註冊的所有 Hook。清單會依事件分組,並顯示每個 Hook 的來源檔案與 matcher。此選單為唯讀,因此必須編輯設定檔才能變更 Hook。檔案監控程式通常會自動偵測變更,不需要重新啟動。
Claude Code 有哪些 hook 事件
Release 2.1.232 列出 31 個事件,範圍從 SessionStart 到 SessionEnd,涵蓋壓縮、子代理程式、worktree 與設定檔。伺服器管理通常只會用到其中幾個。
PreToolUse:工具呼叫執行前觸發。只有這個事件可以阻止執行。PostToolUse:工具呼叫成功後觸發。失敗時則觸發PostToolUseFailure;因此,必須查看所有結果的 hook 需要同時設定這兩個事件。PermissionRequest:工具呼叫需要權限決策時觸發,也就是原本會顯示核准提示的時機。UserPromptSubmit:提交提示時、Claude 處理提示前觸發。此 hook 寫入 stdout 的內容會加入模型的 context。SessionStart與SessionEnd:分別在工作階段的開始與結束時觸發。壓縮後也會觸發SessionStart,此時 matcher 值為compact。Stop:Claude 完成回應時觸發。每個回合觸發一次,不是每個完成的工作觸發一次。
每個事件群組都有 matcher,用來決定哪些事件會執行 hook。對工具事件而言,它會依工具名稱篩選,因此 "Edit|Write" 只會在編輯檔案時觸發,不會在其他情況觸發。Matcher 區分大小寫。空的 matcher 會在每次事件發生時觸發。來自 MCP(model context protocol)伺服器的工具名稱為 mcp__<server>__<tool>;因此,使用 "mcp__github__.*" 作為 matcher,可以只比對其中一台伺服器的工具,不會比對其他伺服器。
Stop hook 有一個在撰寫前必須注意的陷阱。會阻止執行的 Stop hook 會讓模型重新工作;連續阻止 8 次後,Claude Code 會覆寫該 hook。請從 hook 輸入讀取 stop_hook_active 欄位,若其值為 true 就結束並傳回 0,否則 hook 會持續迴圈,直到達到這項上限。
Hook 從標準輸入接收的內容
當 Claude 即將執行 npm test 時,Bash 上的 PreToolUse hook 會從標準輸入讀取以下內容:
{
"session_id": "abc123",
"cwd": "/home/deploy/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}每個事件都包含 session_id、cwd、permission_mode、transcript_path 和 hook_event_name。工具事件還會加入 tool_name、tool_input 和 tool_use_id。其他事件則包含各自的欄位:UserPromptSubmit 會取得 prompt 文字,而 SessionStart 會取得由 startup、resume、clear、compact 或 fork 組成的 source。
jq 是在 shell script 中讀取這些內容的常用工具,但精簡的伺服器映像檔通常不會預先安裝。請先在 Ubuntu 和 Debian 上使用 sudo apt install -y jq 安裝。
執行狀態碼對目前工具呼叫的影響
共有三種結果。
- Exit 0 表示 hook 未提出異議。在
PreToolUse中,這不等同於核准,正常的權限流程仍會執行。在UserPromptSubmit和SessionStart中,stdout 會加入模型的上下文。 - Exit 2 會阻擋可被阻擋的事件,包括
PreToolUse,而 stderr 會成為顯示給模型的原因。對於無法阻擋的事件,例如PostToolUse,阻擋會被忽略,但 stderr 仍會以回饋形式傳給模型。 - 任何其他執行狀態碼 都代表不會阻擋流程的錯誤。動作會繼續執行。逐字稿會顯示 hook 錯誤通知,其中包含 stderr 的第一行,並接在
Failed with non-blocking status code:文字之後。
若需要阻擋或保持靜默以外的行為,請使用 Exit 0,改將 JSON 物件輸出至 stdout。PreToolUse hook 會透過 permissionDecision 做出決定:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Database drops go through a migration, not through the agent."
}
}"allow" 會略過互動式提示,"deny" 會取消呼叫並將原因傳給模型,"ask" 則會照常顯示提示。每個 hook 應選用一種方式。若在 stdout 輸出 JSON 決策的同時使用 Exit 2,最後會得到必須另外查閱才能判讀的結果。
當多個 hook 符合同一事件時,這些 hook 會平行執行,且每一個都會執行至完成。某個 hook 的 deny 不會停止其同層的其他 hook,因此記錄 hook 仍會寫入記錄,而防護機制 hook 會拒絕相同的呼叫。Claude Code 接著會合併各項結果,並依拒絕、延後、詢問、允許的順序,保留限制最嚴格的結果。
範例 1:在破壞性命令執行前加以阻擋
將以下內容儲存為 .claude/hooks/block-destructive.sh,放在專案中:
#!/bin/bash
# Deny a Bash tool call whose command matches a banned pattern.
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
for pattern in 'rm -rf /' 'mkfs' 'dd if=' 'DROP TABLE'; do
if printf '%s' "$COMMAND" | grep -qiF -- "$pattern"; then
echo "Blocked by policy: the command matches '$pattern'. A human runs this one." >&2
exit 2
fi
done
exit 0將其設為可執行檔,然後在 .claude/settings.json 的 PreToolUse 中註冊:
chmod +x .claude/hooks/block-destructive.sh{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh",
"timeout": 10,
"statusMessage": "Checking the command against policy"
}
]
}
]
}
}在信任此腳本前,先手動測試。因為 hook 若因自身輸入而當機,會失效開放:
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /var/lib/postgresql"}}' \
| .claude/hooks/block-destructive.sh
echo $?您應在 stderr 看到 Blocked by policy: 行,且結束代碼為 2。輸入無害的命令,例如 ls -la,應不會看到任何輸出,且結束代碼為 0。在工作階段中,被拒絕的呼叫會出現在 transcript 中,並以您的訊息作為原因;模型會讀取該訊息並調整行為。
有一項特性使這樣做值得:PreToolUse hook 會在權限模式檢查前觸發,且適用於所有權限模式,因此即使在 bypassPermissions 下,deny 仍然有效。這也是 hook 適合搭配 Claude Code 自動模式及其權限設定 使用的原因;即使降低提示限制,hook 仍會觸發。
請正確認識這項機制的限制。針對命令字串進行模式比對,可以防止 agent 粗心操作,但無法防止 agent 刻意繞過,因為相同的命令可以改用 grep 看不到的形式表示。強制性規則應放在權限系統中,並由程序執行所在的帳號加以限制。
範例 2:每次編輯後格式化並執行 lint
PostToolUse 搭配 Edit|Write 比對器,會在任何檔案編輯工具執行後觸發。將以下內容儲存為 .claude/hooks/after-edit.sh:
#!/bin/bash
# Format the edited file, then report lint failures back to the model.
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
case "$FILE" in
*.py)
ruff format "$FILE" >/dev/null 2>&1
if ! ruff check "$FILE" >&2; then
exit 2
fi
;;
*.sh)
if ! shellcheck "$FILE" >&2; then
exit 2
fi
;;
esac
exit 0{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/after-edit.sh",
"timeout": 60
}
]
}
]
}
}請 Claude 將縮排錯誤的函式加入 Python 檔案,然後開啟該檔案。檔案會以格式化後的內容顯示。這就能確認 hook 已執行,因為 hook 成功時不會在對話中顯示任何內容。
這裡的 exit 2 不會復原任何變更。PostToolUse 會在工具執行完畢後才觸發,因此無論如何,編輯結果都已寫入磁碟。exit 2 的作用是讓 ruff check 的輸出以回饋形式傳給模型,使模型修正剛才引入的錯誤,而不是繼續執行。這就是在 commit 時才發現 lint 失敗,與 agent 在同一回合中修正錯誤的差異。
這裡有兩項 matcher 限制需要注意。Edit|Write 看不到 shell command 修改的檔案,而 Claude 經常透過 Bash 寫入檔案,因此這項缺口確實存在。若要涵蓋每次呼叫,請同時比對 Bash,並讓 script 使用 git status --porcelain 列出變更的檔案。若要每回合只涵蓋一次,請改將掃描放在 Stop hook 中。
範例 3:記錄每次工具呼叫以供稽核
PostToolUse 上的空白比對器會套用至每個工具。將記錄送至系統日誌,而不是寫入家目錄中的檔案,可避免代理程式透過自己的 shell 存取:
{
"hooks": {
"PostToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "jq -c '{time: now|todate, session: .session_id, cwd: .cwd, tool: .tool_name, input: .tool_input}' | logger -t claude-code -p local0.info"
}
]
}
]
}
}使用 journalctl -t claude-code -o cat | tail -n 5 讀取記錄。您應該會看到每次工具呼叫各佔一行 JSON,最新的記錄位於最後。若完全沒有輸出,表示 hook 未執行;下方的疑難排解章節會說明相關問題。
在 PostToolUseFailure 下方加入相同區塊,以記錄失敗的呼叫,因為 PostToolUse 只會在成功時執行,而失敗的命令通常才是需要關注的項目。使用 logger,而不是將內容附加至家目錄中的檔案,原因在於檔案擁有權:hook 會以與代理程式 shell 相同的使用者身分執行,因此該使用者能附加內容的檔案,也能將其截斷。日誌則由 systemd-journald 以其專用帳號寫入。
Hook 可執行多久
The data behind this chart
[
{
"label": "command, http or mcp_tool hook",
"default_timeout_seconds": 600
},
{
"label": "agent hook",
"default_timeout_seconds": 60
},
{
"label": "prompt hook",
"default_timeout_seconds": 30
},
{
"label": "command hook on UserPromptSubmit",
"default_timeout_seconds": 30
},
{
"label": "command hook on MessageDisplay",
"default_timeout_seconds": 10
},
{
"label": "any hook on SessionEnd",
"default_timeout_seconds": 1.5
}
]命令 hook 預設可執行 600 秒,也就是 10 分鐘。部分事件會大幅縮短這段時間。SessionEnd hooks 共用 1.5 秒的總預算,因此工作階段結束時的清理工作必須迅速完成;但在 hook 上設定較長的 timeout,即可將共用預算提高到相同時間,最長為 60 秒。
超過逾時時間的 hook 會遭到取消,且不會產生決策。對 PreToolUse guardrail 而言,這表示它不會阻擋操作:工具呼叫會繼續進入一般權限流程。因此,guardrail script 應保持精簡。對於不需要等待的耗時工作,例如將 log 傳送到其他位置,請設定 "async": true,讓 hook 在背景執行,而不會阻塞工具呼叫。
Hooks、rules 檔案、skills 與 MCP servers
這 4 種機制都會改變 agent 的行為,因此經常彼此混淆。但只有其中 1 種不會停留在建議層級。
rules 檔案(CLAUDE.md,或 .claude/rules/ 下的檔案)是載入 model context 的文字。它會塑造行為,但不會強制執行。在長對話、大型 diff 與新的使用者請求中,rules 檔案的其中一行可能被忽略。這就是 agent 忽略你寫下的指示 的一般原因。
skill 是由指示與 script 組成的資料夾。model 判定某項 skill 相關時,才會載入它。這項判定正是 skill 的用途,也是它的限制:仍由 model 做決定。以 Ponytail,它會引導 agent 採用能運作的最小變更 為例,你可以看到 skill 的兩面。它能以 hook 無法做到的方式,塑造整項工作的處理方式,但只會在 model 選擇載入它時生效。
MCP(model context protocol)server 會提供 model 可呼叫的新工具。它能擴大 agent 可存取的範圍,但不會讓 agent 主動使用任何工具。此外,MCP server 是必須自行維運的獨立程序,這本身就是一項工作:請參閱 在 VPS 上執行 MCP servers。
Hook 是 4 種機制中唯一不需要 model 選擇就會執行的機制。偏好設定使用 rules 檔案;model 在適用時應遵循的程序使用 skill。每次都必須執行的步驟,或絕對不允許發生的事項,則使用 hook。更深入的比較,包括 skill 何時優於 rules 檔案,請參閱 skills、MCP 與 rules 檔案的比較。
Plugin 是封裝方式,不是第 5 種機制。它會將 hooks 與 skills 封裝成一個可安裝的單位。團隊可藉此將相同的防護措施部署到每台機器:請參閱 Claude Code plugins 的運作方式。
共用 VPS 上的安全決策
Hook 是由 agent 觸發並執行的程式碼。它會以啟動 Claude Code 的使用者身分執行,並繼承該使用者的環境與檔案權限。在筆電上,這屬於工作流程問題。在無人值守執行 agent 的 VPS 上,這是安全問題,實際上可分為 4 個部分。
儲存庫中的 hook 不是你撰寫的程式碼。 .claude/settings.json 會隨儲存庫提交,因此複製儲存庫並在其中啟動工作階段,可能會註冊儲存庫附帶的 hook。Claude Code 會在該資料夾的 workspace trust 對話框後方控管 project hook。這表示接受信任的同時,也是在決定執行這些 hook。請先閱讀 hooks 區塊。
Hook 可以看到完整的工具輸入。 會記錄 tool_input 的 audit hook,會將每個命令的所有引數寫入檔案,包括恰好出現在命令列上的任何 token。該日誌需要與 secret 受到相同保護。這也是 讓 secret 不落入 AI agent 可觸及範圍 這個更廣泛問題的一部分。
Hook 可以將內容寫入模型的上下文。 SessionStart 或 UserPromptSubmit hook 輸出至 stdout 的任何內容,都會加入對話。從外部來源、issue tracker 或 log 檔案讀入文字的 hook,會將不受信任的文字交給模型,效果就像是你親自輸入的一樣。將 同一 VPS 上另一個 Claude Code 工作階段 的備註轉送過來的 hook 也是如此。某個 agent 的輸出,並不比 issue tracker 的內容更值得信任。請將這些 stdout 內容視為輸入,而不是輸出。
真正的控制點是權限。 請以專用的非特權使用者執行 agent,並只授予它所需的 sudo 規則。設定 PreToolUse deny 仍然有價值,但這項機制本來就只提供盡力而為的控制:參考文件對 if filter 也有相同說明,並要求在需要強制拒絕時使用 permission system。能在高壓情況下持續生效的是 permission 規則,以及執行該程序的使用者帳號。
有一項特性在所有設定中都成立。PreToolUse hook 會在每種 permission mode 中的 permission-mode 檢查之前觸發,因此 hook 回傳 deny 時,即使在 bypassPermissions 下也會阻擋工具。Hook 可以收緊 permission 規則允許的操作,但無法放寬這些規則。
為什麼我的 hook 沒有觸發?
請依照以下順序檢查。每個步驟都說明你實際會看到的症狀。
- 執行
/hooks,確認 hook 是否出現在預期的事件下。hook 未出現在選單中,通常表示設定檔有 JSON 語法錯誤,因為不允許使用尾隨逗號和註解;也可能表示檔案不在上方列出的 6 個位置之一。 - 逐字比對 matcher 與工具名稱。Matcher 區分大小寫,因此
"bash"永遠不會比對到Bash工具。 - 使用上方範例 1 的方式,搭配範例輸入手動執行指令碼。非預期的結束代碼表示指令碼本身有錯誤,而 Claude Code 會將其回報為 hook 錯誤,而不是決策結果。
- 顯示
jq: command not found的通知表示該機器缺少jq。如果你自己的指令碼顯示command not found,表示路徑解析失敗,請使用${CLAUDE_PROJECT_DIR}或絕對路徑。如果指令碼完全沒有執行,通常是因為沒有執行權限。 - hook 印出有效的 JSON,但沒有任何效果。Shell 形式的 hook 會透過
sh -c執行;如果 shell profile 印出橫幅文字,該文字就會附加在 JSON 前方。標準輸出不再以{開頭,因此 Claude Code 會將整段內容視為純文字,並忽略決策。在結束代碼為 0 時,除了 debug log 之外,不會在任何地方回報錯誤。請在 profile 中包住任何echo,使其只在互動式 shell 中執行。 - 仍然無法排除問題時,請使用
claude --debug-file /tmp/claude.log啟動工作階段,並在第二個終端機中執行tail -f /tmp/claude.log。Debug log 會記錄哪些 hook 符合條件、各 hook 回傳的結束代碼,以及它們寫入標準輸出和標準錯誤的所有內容。
FAQ
Claude Code hook 與 CLAUDE.md instruction 有何差異?
CLAUDE.md instruction 是模型內容的一部分,因此會與對話及目前的請求競爭注意力,模型也可以權衡其與這些內容的優先順序。hook 則是 Claude Code 在生命週期固定階段執行的 shell command,因此每次事件發生時都會執行,不受模型決定影響。偏好設定應使用 instruction。必須一律執行的步驟,或絕對禁止執行的動作,應使用 hook。
如何阻止 Claude Code 執行特定的 shell command?
註冊 PreToolUse hook,並設定 Bash matcher,從 .tool_input.command 讀取 command,將原因寫入 stderr,然後以 2 結束。Claude Code 會取消該呼叫,並將原因顯示給模型。這會在權限模式檢查前發生,因此即使在 bypassPermissions mode 中,拒絕仍然有效。對 command string 進行模式比對只能作為防護措施,不能視為安全邊界,因為相同的 command 可能以模式無法比對的形式撰寫。因此,還應搭配 permission rules 及非特權帳號使用。
hook 輸出有效 JSON,但沒有任何作用。為什麼?
最常見的原因是 shell profile。未設定 args 欄位的 hook 會透過 sh -c 執行,而某些 profile 會在每個 shell 啟動時輸出 banner,導致該內容出現在 JSON 前面的 stdout。由於輸出不再以 { 開頭,Claude Code 會將全部內容視為純文字並忽略該決策;此外,若以 0 結束,transcript 中完全不會顯示任何內容。請在 profile 中以互動式 shell 檢查包住任何 echo,然後從 claude --debug-file /tmp/claude.log 讀取 debug log,以確認問題已修正。
在共用伺服器上執行 Claude Code hook 是否安全?
hook 會以啟動 Claude Code 的使用者身分執行,並具備該使用者的檔案權限,因此 hook 能執行該帳號可執行的所有操作。以下兩項做法可涵蓋大多數風險:使用具有限制性 sudo policy 的專用非特權帳號執行 agent;接受 repository 的 workspace trust 對話框前,先閱讀其中的 hooks block,因為 project hook 會隨 .claude/settings.json 一併提供。若不希望執行任何此類 hook,請在 settings file 中設定 "disableAllHooks": true。