用 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 fmtrustfmtblack、甚至自家寫的 sanity check script 都是同一個套路。

什麼是 Claude Code Hooks

Hooks 是 Claude Code 在執行流程中暴露的事件鉤子。當 Claude 想呼叫某個工具(例如 EditWriteBash),或者一個工具剛跑完、或者本次對話即將結束,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

除此之外還有 NotificationSessionStartPreCompact 等比較少用的事件,這篇先聚焦在 PostToolUse + Prettier 這條最實用的組合,其他事件用到時再回頭查官方文件。下面這張圖把一次完整對話過程中幾個主要 hook 事件的觸發時機畫成時序圖,比較容易看出它們各自插在哪個環節:

Claude Code 一次對話中各 hook 事件的觸發時序:UserPromptSubmit、PreToolUse、PostToolUse、Stop 分別插在使用者輸入、工具呼叫前、工具完成後、對話結束等位置
Claude Code 一次對話中各 hook 事件的觸發時序,每個事件都會把當下的 JSON 上下文餵給對應的指令

配置位置與優先順序

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 用 EditWrite 工具碰過一個檔案,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。下圖把這條流程畫出來:

PostToolUse hook 觸發 Prettier 的流程:Claude 改檔 → 工具執行完 → 檢查 hook 設定 → matcher 比對 → if 條件過濾副檔名 → 跑 prettier 或跳過 → 退出碼回到主流程
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 提供更細的指示。

Hook 結束後 Claude Code 依退出碼分支處理:0 視為成功並再判讀 stdout JSON、2 阻擋並把 stderr 回灌、其他非零顯示但不阻擋
退出碼與 JSON 輸出共同決定 Claude Code 下一步

單純跑 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} 只有寫在 argscommand 字串裡才會被 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 工具,差別只在 commandif 過濾條件。一些常見組合:

  • 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 依序跑 blackisort,配 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 testpytest,產生報告寫進 .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 等比較進階的形式,等基本流程跑順了再回頭看會比較有感覺。


Sponsored Links