# Claude Code Hooks 完整教學:讓 AI 助手自動化你的開發流程

那天我又忘了格式化代碼。
說起來有點丟臉,但這種事真的常發生。用 Claude Code 寫完一段功能,測試通過,開心地準備提交,結果 CI 紅了——ESLint 報了一堆格式問題。我只好默默地跑 `prettier --write`,然後再提交一次。
更慘的是有一次,我讓 Claude 幫忙改一個配置,結果它「順手」把 `.env` 文件也改了。雖然我的 `.env` 有在 `.gitignore` 裡,但當下那個心跳漏拍的感覺,你懂的。
後來我才知道,這些問題其實都有一個優雅的解決方案:**Hooks**。
## 什麼是 Hooks?用管家來比喻
怎麼解釋 Hooks 呢?想像你請了一個超級能幹的 AI 助手(就是 Claude),但這個助手有時候會太有創意,或者忘記一些你很在意的細節。
Hooks 就像是你設定的一套「家規」或者說「自動化管家」。它會在特定時機自動執行你預設的動作:
- Claude 要寫入文件**之前**,管家先檢查:「等等,這是不是敏感文件?」
- Claude 寫完文件**之後**,管家自動跑:「來,我幫你格式化一下。」
- Claude 完成整個任務**的時候**,管家通知你:「老闆,活幹完了!」

關鍵在於:Hooks 是**確定性**的。
這很重要。AI 模型是機率性的——你告訴 Claude「記得格式化代碼」,它大部分時候會記得,但偶爾就是會忘。而 Hooks 不一樣,只要你設定了,它就一定會執行。不是「應該會」,是「一定會」。
這個差別,在團隊協作或者處理敏感操作時,真的很關鍵。
## Hook 事件:在對的時機做對的事
Claude Code 目前支援這些 Hook 事件,每個都有它適合的使用場景:
**PreToolUse** 是最常用的之一。它在 Claude 執行任何工具(寫檔案、跑命令等)**之前**觸發。厲害的是,它可以**阻止**這個操作。想像它是一個門衛,看到可疑的人可以直接擋在門外。
**PostToolUse** 則是在工具執行**之後**觸發。它沒辦法阻止操作(因為已經執行了),但很適合做後續處理,像是自動格式化、跑測試、寫日誌這類事情。
**Stop** 在 Claude 完成整個回應時觸發。適合做最後的驗證或者發送通知。
還有一些其他的:**UserPromptSubmit**(用戶提交提示時)、**SessionStart**(會話開始時)、**Notification**(發送通知時)等等。不過剛入門的話,先掌握前三個就夠用了。
## 動手配置第一個 Hook
好,說了這麼多,來實際操作一下。
配置 Hooks 有兩種方式。如果你喜歡圖形介面,可以在 Claude Code 裡直接輸入 `/hooks`,它會引導你一步步設定。但我個人更喜歡直接編輯設定檔,因為可以版本控制,也方便在團隊間分享。
設定檔的位置有三個選擇:
- `~/.claude/settings.json`:全域設定,所有專案都會套用
- `.claude/settings.json`:專案設定,可以 commit 到 repo 跟團隊共用
- `.claude/settings.local.json`:本地設定,通常放在 `.gitignore` 裡
來寫一個最簡單的 Hook:每次 Claude 編輯 TypeScript 文件後,自動跑 Prettier 格式化。
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs -I {} sh -c 'echo {} | grep -q \"\\.ts$\" && npx prettier --write {}'"
}
]
}
]
}
}
```
這段設定做了什麼?當 Claude 使用 `Write` 或 `Edit` 工具後(`matcher` 用 `|` 表示「或」),它會讀取被操作的文件路徑,如果是 `.ts` 結尾的文件,就跑 Prettier 格式化。
看起來有點複雜?讓我拆解一下那個 command:
1. `jq -r '.tool_input.file_path'`:從 Hook 收到的 JSON 資料中提取文件路徑
2. `xargs -I {} sh -c '...'`:把路徑傳給後面的 shell 命令
3. `grep -q "\\.ts$"`:檢查是不是 TypeScript 文件
4. `npx prettier --write {}`:如果是的話,格式化它
其實如果邏輯稍微複雜一點,我建議直接寫成獨立的 shell 腳本,然後在 Hook 裡呼叫它。比較好維護,也比較好除錯。

## 實戰案例:我每天都在用的三個 Hook
### 案例一:保護敏感文件
這個是我設定的第一個 Hook,來自那次 `.env` 被誤改的教訓。
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "python3 -c \"import json,sys; d=json.load(sys.stdin); p=d.get('tool_input',{}).get('file_path',''); sys.exit(2 if any(x in p for x in ['.env','.pem','.key','secrets']) else 0)\""
}
]
}
]
}
}
```
這段程式會檢查 Claude 想要寫入的文件路徑,如果包含 `.env`、`.pem`、`.key` 或 `secrets` 這些關鍵字,就會回傳 exit code 2,阻止操作。
對了,exit code 很重要:
- **0**:允許操作繼續
- **2**:阻止操作,並把 stderr 的內容回饋給 Claude
所以你可以在腳本裡輸出一些提示訊息到 stderr,Claude 會看到並理解為什麼被阻止。
### 案例二:自動跑測試
每次改完代碼手動跑測試?太累了。
```json
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "npm test 2>&1 | head -50 || true"
}
]
}
]
}
}
```
這個 Hook 在 Claude 完成回應時觸發,自動跑測試。`head -50` 是為了避免輸出太長,`|| true` 是確保即使測試失敗也不會阻塞 Claude。
如果你只想在特定文件被修改時才跑測試,可以用 PostToolUse 搭配更精細的條件判斷。
### 案例三:完成通知
這個真的超實用。有時候讓 Claude 跑一個比較長的任務,我會切去做別的事,但又怕錯過它完成的時機。
macOS 用戶可以這樣設:
```json
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"任務完成!\" with title \"Claude Code\"'"
}
]
}
]
}
}
```
Linux 用戶可以用 `notify-send`:
```bash
notify-send "Claude Code" "任務完成!"
```
每次 Claude 完成回應,系統通知就會跳出來。再也不用一直盯著終端機了。
## 踩坑經驗:我替你試過的那些錯
### 小心無限循環
這個坑我踩過。假設你設了一個 PostToolUse Hook,在 Claude 寫入文件後自動格式化。格式化工具本身也會「寫入」文件,這會不會又觸發 Hook?
好消息是,Claude Code 有做處理,Hook 觸發的操作不會再次觸發 Hook。但如果你的 Hook 邏輯比較複雜,還是要小心。
### 性能考量
Hook 是同步執行的,意思是 Claude 會等 Hook 跑完才繼續。如果你的 Hook 要跑很久(比如完整的測試套件),Claude 就會卡在那邊。
解法是讓 Hook 腳本在背景執行,或者只跑快速的檢查。比如只跑被修改文件相關的測試,而不是整個測試套件。
### 除錯技巧
Hook 沒有如預期執行?試試這幾招:
1. 用 `claude --debug` 啟動,可以看到詳細的 Hook 執行日誌
2. 在 Hook 腳本裡加 log,輸出到一個文件
3. 先用簡單的 `echo` 命令測試,確認 Hook 有被觸發
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo \"Hook triggered at $(date)\" >> /tmp/hook-debug.log"
}
]
}
]
}
}
```
跑幾次操作後,檢查 `/tmp/hook-debug.log` 看看有沒有內容。
### Matcher 的語法
Matcher 支援幾種寫法:
- 精確匹配:`"Bash"` 只匹配 Bash 工具
- 正則表達式:`"Write|Edit"` 匹配 Write 或 Edit
- 萬用:`""` 或不設定,匹配所有工具
如果你的 Hook 沒觸發,先檢查 matcher 是不是寫對了。
## 進階玩法:Prompt 型 Hook
除了 command 類型,Hook 還支援 prompt 類型。這種 Hook 不是執行 shell 命令,而是讓 Claude 用 LLM 來決定怎麼處理。
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "prompt",
"prompt": "檢查這個 bash 命令是否安全。如果包含 rm -rf、sudo 或其他危險操作,請阻止並解釋原因。"
}
]
}
]
}
}
```
這種方式更靈活,可以處理不好用規則定義的情況。但相對的,它會消耗額外的 token,而且因為是 LLM 判斷,所以結果可能不是 100% 確定性的。
我的建議是:能用 command 就用 command,規則清楚又快。只有真的需要「理解」內容時,才考慮 prompt 型 Hook。
## 團隊協作:分享你的 Hook 配置
如果你在團隊裡工作,可以把 Hook 配置放在專案的 `.claude/settings.json` 裡,然後 commit 到 repo。這樣團隊成員都會自動套用相同的規則。
但要注意一點:確保 Hook 裡用到的工具(比如 prettier、eslint)在大家的環境裡都有安裝。不然 Hook 會執行失敗,可能造成困惑。
一個好的做法是在專案的 README 或 CLAUDE.md 裡說明:
```markdown
## Claude Code 設定
本專案使用 Claude Code Hooks 自動化以下流程:
- 編輯 TypeScript 文件後自動格式化
- 阻止寫入敏感配置文件
- 任務完成時發送通知
請確保已安裝:`npm install -D prettier eslint`
```
## 該你動手了
說了這麼多,最好的學習方式還是動手試試看。
我建議從最簡單的開始:設一個 PostToolUse Hook,在 Claude 編輯文件後 echo 一句話。確認它有觸發之後,再慢慢加入你真正想要的邏輯。
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "echo '文件已更新' && date"
}
]
}
]
}
}
```
先從這個開始,然後根據你的實際需求慢慢擴展。
Hooks 真的是 Claude Code 裡一個被低估的功能。一旦你開始用了,就會發現很多以前需要手動做的事情,現在都可以自動化了。那種「設定好就不用再管」的感覺,真的很爽。
如果你設定了什麼有趣的 Hook,歡迎分享出來。這種小工具的妙用,往往是社群互相交流才會發現的。
---
## 參考資源
- [Claude Code Hooks 官方文件](https://code.claude.com/docs/en/hooks)
- [Hooks 入門指南](https://code.claude.com/docs/en/hooks-guide)
- [GitButler: Automate Your AI Workflows with Claude Code Hooks](https://blog.gitbutler.com/automate-your-ai-workflows-with-claude-code-hooks)
- [GitHub: claude-code-hooks-mastery](https://github.com/disler/claude-code-hooks-mastery)
---