完整解析 Claude Code Hooks 機制,從五種類型到退出碼控制,搭配四個真實配置案例,讓 AI 操作可審計、可攔截、可自動化。
每次 Claude Code 讀檔案、寫程式碼、執行指令,背後都有一套事件系統在運作。Hooks 就是接入這套系統的介面——你可以在任意時機注入自己的邏輯,自動做程式碼檢查、記錄日誌、攔截危險操作,或者觸發任何 shell 指令。
這篇文章把 Hooks 的機制、設定方式和實戰用法完整講一遍。
Hooks 是設定在 settings.json 裡的 shell 指令,Claude Code 在特定事件發生時自動執行它們。
最直接的類比:git hooks。git 在 commit、push 等操作前後可以觸發腳本,Claude Code Hooks 的思路完全一樣,只是觸發點是 AI 的工具呼叫。
為什麼這很重要?
Claude Code 的能力越強,你越需要確定性的控制層。Hooks 提供的是:
- 不依賴 prompt 的執行保證(Claude 可能忽略指令,但 hook 一定跑)
- 可審計的操作記錄
- 自動化的品質檢查
| 類型 | 觸發時機 | 典型用途 |
|---|---|---|
PreToolUse |
工具呼叫前 | 攔截危險操作、記錄意圖 |
PostToolUse |
工具呼叫後 | 自動 lint、執行測試 |
PreCompact |
上下文壓縮前 | 儲存當前狀態快照 |
Notification |
Claude 發出通知時 | 桌面推播、Slack 訊息 |
Stop |
Claude 完成回覆時 | 彙整日誌、觸發後續流程 |
最常用的是 PreToolUse 和 PostToolUse,圍繞工具呼叫做攔截和後處理。
Hooks 寫在 ~/.claude/settings.json(全域)或專案根目錄的 .claude/settings.json(專案級)裡:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npm run lint --silent"
}
]
}
]
}
}
三個核心欄位:
matcher:匹配工具名的正規表示式,決定這個 hook 在哪些工具呼叫後觸發。"Write|Edit" 表示寫檔案或編輯檔案時觸發。留空或 ".*" 匹配所有工具。type:目前只有 "command"。command:任意 shell 指令。寫 matcher 時需要知道工具叫什麼名字:
| 工具名稱 | 對應操作 |
|---|---|
Write |
寫入新檔案 |
Edit |
編輯檔案 |
Bash |
執行 shell 指令 |
Read |
讀取檔案 |
Glob |
檔案搜尋 |
Grep |
內容搜尋 |
TodoWrite |
更新任務清單 |
執行時透過 stdin 傳入 JSON:
{
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/file.rb",
"content": "..."
},
"tool_response": "..."
}
PreToolUse 拿到 tool_input(呼叫前的入參),PostToolUse 同時拿到 tool_response(工具回傳結果)。
可以用這些資料寫有判斷邏輯的 hook:
#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path')
# 只對 .rb 檔案執行 rubocop
if [[ "$file" == *.rb ]]; then
rubocop "$file" --autocorrect-all --no-color
fi
Hook 的退出碼控制 Claude Code 的後續行為:
| 退出碼 | 含義 |
|---|---|
0 |
成功,繼續執行 |
2 |
阻斷:取消當前工具呼叫,將 stderr 輸出回饋給 Claude |
| 其他非零 | 記錄錯誤,但繼續執行 |
退出碼 2 是最有用的——它讓你可以在 PreToolUse 裡寫攔截邏輯,阻止 Claude 執行某個操作,並告訴它原因。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash -c 'file=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .file_path 2>/dev/null); [[ \"$file\" == *.rb ]] && rubocop -A \"$file\" --no-color -q || true'"
}
]
}
]
}
}
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash -c 'cmd=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .command); if echo \"$cmd\" | grep -qE \"rm.*/(migrations|seeds)\"; then echo \"禁止刪除 migrations 或 seeds 目錄\" >&2; exit 2; fi'"
}
]
}
]
}
}
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|Bash",
"hooks": [
{
"type": "command",
"command": "echo \"$(date '+%Y-%m-%d %H:%M:%S') $CLAUDE_TOOL_NAME: $(echo $CLAUDE_TOOL_INPUT | jq -c .)\" >> ~/.claude/audit.log"
}
]
}
]
}
}
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude 已完成\" with title \"Claude Code\"'"
}
]
}
]
}
}
兩個位置可以放 Hooks:
全域(~/.claude/settings.json):適合所有專案通用的規則,比如日誌、通知。
專案級(.claude/settings.json):適合專案專屬的檢查。專案級 hooks 和全域 hooks 會合併執行,不會互相覆蓋。
團隊專案建議把專案級 settings.json 提交到 git,這樣所有人都跑相同的 hooks。
Hook 不生效時的排查步驟:
write 不等於 Writeecho "hook triggered" >> /tmp/hook.log 確認觸發Hooks 的核心價值是把不確定的 AI 行為和確定的工程規範接在一起。Claude 可能忘記執行測試,但 PostToolUse hook 不會。Claude 可能誤刪檔案,但 PreToolUse 攔截不會放行。
從一個 hook 開始:寫檔案後自動 lint。跑順了再加其他的。