用 Claude Code 寫一段時間之後,多半會碰到一個小困擾:它寫完的程式碼縮排不一致、引號 single double 混用、行尾沒有逗號補上——功能上沒問題,但對遵守 Prettier、ESLint 規則的專案來說每次都要再手動格式化一次。其實 Claude Code 內建有一個叫 Hooks 的機制,可以讓開發者在「Claude 即將呼叫工具」「Claude 剛完成一個動作」「Claude 結束本次對話」等時機點,掛上自訂的 shell 指令或 script。把 PostToolUse 接到 Prettier 之後,Claude 每次 Edit 或 Write 完一個檔案,這個檔案就會自動跑一遍格式化,下次 git diff 看到的就是已經整理好的版本。
這篇用 Prettier 當例子走過 hooks 的設定方式,包含什麼是 hooks、有哪幾個事件可以掛、settings.json 的配置語法、matcher 與 if 條件如何過濾、退出碼與 JSON 輸出的差別、以及實務上偵錯的方向。學會這套機制之後,把 Prettier 換成 ESLint、go fmt、rustfmt、black、甚至自家寫的 sanity check script 都是同一個套路。
什麼是 Claude Code Hooks
Hooks 是 Claude Code 在執行流程中暴露的事件鉤子。當 Claude 想呼叫某個工具(例如 Edit、Write、Bash),或者一個工具剛跑完、或者本次對話即將結束,Claude Code 會檢查 settings.json 裡有沒有對應事件的 hook 設定,有的話就把當下的上下文(tool name、檔案路徑、tool 回傳結果等)以 JSON 形式從 stdin 餵給 hook 指定的 shell 指令執行。指令的 stdout、stderr、exit code 會反向影響 Claude 的後續決定。
從應用層次來看,常見的事件有以下幾種:
- PreToolUse:Claude 即將呼叫工具,但還沒實際執行。適合做「擋住危險操作」「驗證輸入」「依條件改寫工具參數」。例如禁止
rm -rf、禁止編輯敏感檔案、強制改寫某些路徑(完整實作另見 Claude Code PreToolUse Hook 教學) - PostToolUse:工具已執行完,結果已產生。適合做「事後清理」「自動格式化」「跑測試」「寫紀錄」。本文要用的 Prettier 就掛在這個事件上
- UserPromptSubmit:使用者剛送出新訊息,Claude 還沒開始思考。可以在這時注入額外的上下文(例如環境變數、目前的 git branch)給 Claude 參考
- Stop / SubagentStop:本次對話結束、或某個 sub-agent 完成。適合做收尾工作,例如送 desktop 通知、寫 audit log
除此之外還有 Notification、SessionStart、PreCompact 等比較少用的事件,這篇先聚焦在 PostToolUse + Prettier 這條最實用的組合,其他事件用到時再回頭查官方文件。下面這張圖把一次完整對話過程中幾個主要 hook 事件的觸發時機畫成時序圖,比較容易看出它們各自插在哪個環節:

配置位置與優先順序
Hooks 的設定寫在 Claude Code 的 settings.json 裡,三個位置依優先序排列:
| 位置 | 適用範圍 |
|---|---|
~/.claude/settings.json | 個人全域:所有專案共用,例如「凡是寫 .ts 都跑 Prettier」 |
.claude/settings.json | 專案層級:會被 git 追蹤、整個團隊共用,適合「這個 repo 才有的規則」 |
.claude/settings.local.json | 本機 override:通常加進 .gitignore,給個人開發機臨時調整用 |
實務上選擇的原則是:「想全公司專案都自動跑的格式化」放個人全域;「這個專案才需要的 lint / formatter」放專案 .claude/settings.json 並 commit 進 git,讓團隊成員 clone 之後設定就生效;「我自己暫時想關掉某個 hook」放 .claude/settings.local.json,不影響別人。
第一個 Hook:自動跑 Prettier
Prettier 是 JavaScript 生態最廣為使用的 opinionated 程式碼格式化工具,把縮排、引號、換行等風格細節用預設規則統一,省掉團隊在 code review 爭排版的麻煩。本節示範把它接到 PostToolUse hook,每次 Claude 編輯完檔案就自動跑一次。
先準備好前置條件:
# 確認 prettier 可以從 shell 直接呼叫
prettier --version
# 如果沒裝,最快的方法是用 npm 全域安裝
npm install -g prettier
Prettier 準備好之後,在專案根目錄建立 .claude/settings.json(沒這個資料夾就先 mkdir -p .claude),寫入下面的內容:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "prettier",
"args": ["--write", "${tool_input.file_path}"],
"timeout": 30
}
]
}
]
}
}
這份設定的意思是:每當 Claude 用 Edit 或 Write 工具碰過一個檔案,hooks 系統就會去找這支 Prettier 指令,把剛剛操作的檔案路徑(${tool_input.file_path})當參數丟過去,限制 30 秒內完成。Prettier 會就地改寫檔案、整理縮排與標點符號。如果是在 Claude Code 開著的當下改 settings.json,建議離開重進一次對話讓設定重新載入。
實際跑一次看看:
# Claude Code 對話內
> 幫我在 src/utils.ts 加一個 sleep 函式
⏺ Edit(src/utils.ts)
⎿ Updated 1 file
⏺ Hook: PostToolUse → prettier --write src/utils.ts
⎿ src/utils.ts 30ms
看到 Hook 那一行就代表 Prettier 跑成功了,src/utils.ts 已經是格式化過的版本。git status 會看到只有一個變動的檔案,git diff 也是已經整理好的乾淨 diff,不會夾雜 Claude Code 自己的縮排習慣。
整個過程在 Claude Code 內部其實是幾個判斷依序串起來——工具執行完、讀取 PostToolUse 設定、比對 matcher 與 if 條件、確認檔案符合條件之後才實際呼叫 Prettier。下圖把這條流程畫出來:

用 if 條件針對特定副檔名
上面那份配置會對所有 Edit / Write 過的檔案都呼叫 Prettier,但 Prettier 不認識的副檔名(例如 .go、.py、.rs)會回非 0 退出碼,造成 hook 看起來「失敗了」。比較乾淨的做法是用 if 條件先過濾,只對 Prettier 支援的格式觸發:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"if": "Edit(*.ts)|Edit(*.tsx)|Edit(*.js)|Edit(*.jsx)|Edit(*.json)|Edit(*.md)|Edit(*.css)|Edit(*.html)|Write(*.ts)|Write(*.tsx)|Write(*.js)|Write(*.jsx)|Write(*.json)|Write(*.md)|Write(*.css)|Write(*.html)",
"command": "prettier",
"args": ["--write", "${tool_input.file_path}"],
"timeout": 30,
"statusMessage": "Formatting with Prettier..."
}
]
}
]
}
}
if 接受的是 Claude Code 內建的 permission rule 語法,Edit(*.ts) 代表「用 Edit 工具編輯 .ts 檔」,多條規則用 | 串接。statusMessage 則是在 hook 跑的時候顯示給使用者看的提示字串,省得每次都好奇「現在跑什麼」。
如果只想針對特定目錄觸發(例如只想對 src/ 底下的程式碼自動 format,不想動 scripts/),把規則改成 Edit(src/**/*.ts) 這種 glob 形式即可。
退出碼與 JSON 輸出
Hook 跟 Claude Code 主流程的溝通機制有兩條:
- 退出碼:
0代表成功;2代表阻擋(在 PreToolUse 會擋下工具呼叫,在 PostToolUse 則會把 stderr 內容回灌給 Claude 讓它修正);其他非 0 退出碼是「非阻擋的錯誤」,stderr 會顯示給使用者但不會中斷流程 - JSON 輸出:如果退出碼是 0、stdout 又是合法 JSON,Claude Code 會解析成結構化指令。可以指定
"decision": "block"阻擋、"additionalContext"插入額外的提示文字給 Claude、"systemMessage"顯示警告給使用者、"suppressOutput"隱藏 debug log 等
兩個機制疊在一起的話會像下面這張圖:先看退出碼決定整體方向(成功/阻擋/非阻擋錯誤),若是 0 再看 stdout 有沒有合法 JSON 提供更細的指示。

單純跑 Prettier 用退出碼就夠了,但寫成 script 之後能掌握的事情更多。下面這個版本把 Prettier 包成一支 .claude/hooks/format.sh,在格式化失敗時用 JSON 把錯誤訊息回給 Claude,讓它知道剛剛產生的程式碼有語法問題該修:
#!/usr/bin/env bash
# .claude/hooks/format.sh
# 從 stdin 讀 hook input,跑 Prettier,並用 JSON 回饋結果
set -euo pipefail
# Claude Code 透過 stdin 傳入 JSON,裡面有 tool_input.file_path
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
# 只處理 Prettier 認得的副檔名
case "$file_path" in
*.ts|*.tsx|*.js|*.jsx|*.json|*.md|*.css|*.html|*.yml|*.yaml)
;;
*)
# 跳過:直接 exit 0 不做事
exit 0
;;
esac
# 跑 Prettier,捕捉錯誤輸出
if error=$(prettier --write "$file_path" 2>&1); then
# 成功:用 JSON 回個 additionalContext 給 Claude 知道
jq -n --arg ctx "Formatted $file_path with Prettier" '{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": $ctx
}
}'
exit 0
else
# 失敗:把錯誤訊息送回 Claude,請它修
jq -n --arg err "$error" '{
"decision": "block",
"reason": ("Prettier 失敗:" + $err)
}'
exit 2
fi
對應的 settings.json 改成呼叫這支 script:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/format.sh",
"timeout": 30
}
]
}
]
}
}
記得幫 script 加上執行權限:chmod +x .claude/hooks/format.sh。把 hook 包成 script 的好處是邏輯都集中在 shell 檔,settings.json 只負責「在哪個時機點呼叫誰」,後續要加偵錯 log、跳過特定路徑、改 timeout 都不必動 settings。
偵錯:hook 沒跑、跑了沒效果怎麼辦
實務上設好 hook 之後最容易卡在「為什麼沒有觸發」或「明明跑了 Prettier 但格式還是沒變」。可以從這幾個方向排查:
- 確認設定有被載入:剛改完 settings.json 之後要重新啟動 Claude Code 對話讓設定生效,不然會以為自己改錯但其實只是還沒重載。可以在新對話裡先做一個會觸發 hook 的小動作(例如請 Claude 改一個檔案)確認
- matcher 拼錯:
Edit|Write用 pipe,不是逗號。常見錯誤是寫成"Edit, Write",這會被當成「工具叫做Edit, Write」匹配不到任何工具 - 變數插值:
${tool_input.file_path}只有寫在args或command字串裡才會被 Claude Code 插值。如果是寫在 shell script 裡面就要從 stdin 解析 JSON(用jq),不會自動展開 - PATH 問題:hook 被 Claude Code 用一個比較精簡的環境變數啟動,
~/.zshrc或~/.bashrc設的 PATH 可能不會生效。如果指令是npm install -g裝進 nvm 目錄的 prettier,建議用絕對路徑("command": "/Users/yourname/.nvm/versions/node/v22.x.x/bin/prettier"),或在 script 第一行手動補 PATH - 看 stderr:hook 跑出來的 stderr 在 Claude Code 對話裡會以紅色標示出來;非 0 但非 2 的退出碼也會把 stderr 印出。如果什麼都沒看到,就把 script 裡加幾行
echo "DEBUG: $file_path" >&2把關鍵變數印出
把這個套路延伸到其他工具
學會 PostToolUse + Prettier 之後,同樣的格式可以接到任何 lint / format / test 工具,差別只在 command 與 if 過濾條件。一些常見組合:
- ESLint 自動修:
"command": "eslint", "args": ["--fix", "${tool_input.file_path}"],配if限.ts、.tsx - Go 格式化:
"command": "gofmt", "args": ["-w", "${tool_input.file_path}"],配if限.go - Rust 格式化:
"command": "rustfmt", "args": ["${tool_input.file_path}"],配if限.rs - Python 格式化 + 排序 import:包成一個 script 依序跑
black與isort,配if限.py - PreToolUse 擋住敏感檔案:在 PreToolUse 用
if "Edit(.env)|Edit(*.pem)|Edit(secrets/**)",command印一行錯誤訊息後exit 2,避免不小心把密鑰檔交給 Claude 改(含 deny/ask 模式、保護 main 分支等完整範例見 Claude Code PreToolUse Hook 教學) - Stop hook 跑單元測試:每次對話結束自動跑
npm test或pytest,產生報告寫進.claude/last-test.txt,下次對話開始時讀進來當上下文
Hook 本質上是把「Claude Code 不該記住、但每次都要做的事情」自動化,越用越能讓對話聚焦在「我想做什麼」而不是「順便提醒它別忘了 format」。從 Prettier 這種輕量的 hook 開始累積經驗,等到後面需要寫複雜的 enforce 規則(例如阻擋 PR 改 main 分支、commit 前跑 security scan)時,整套機制已經很熟悉。
想看更完整的 hook 事件列表與 JSON 欄位定義,可以查 Claude Code 官方 hooks 參考文件,裡面也有 HTTP hook、prompt hook、MCP tool hook 等比較進階的形式,等基本流程跑順了再回頭看會比較有感覺。