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