
接下來即將進入 Week4 也是整個系列的最後一週。
那這一週開始我們要做什麼呢?這邊我要來介紹一個 AI 的「生命週期觸發器」:Hooks。
它的概念是掛在 AI 的生命週期上,在特定的時機去觸發特定的指令,這個過程並不會有任何 AI 的判斷,完全是系統層級的自動化。
其實 AI 中有一個所謂的「生命週期」的概念,我知道看到這邊可能許多人會有點覺得...
「蛤?生命週期?AI 也有生命週期?」
簡單來講,AI 在進行任務的過程中,會經歷一些特定的事件,例如:
這四個基本上是 AI 最常用的生命週期事件,當這些事件發生時,我們就可以掛上我們的 Hook,讓它自動執行我們想要的指令。
而這些 Hook 的設定,則是放在專案的設定檔中,通常是 settings.json 的 hooks 區塊中。
那 settings.json 是什麼呢?其實我們前面一直沒有特別聊到它,它就是 AI 的設定檔,權限、Hook 這些設定都是放在這裡面,而它跟 Day 15 講的 SKILL 一樣,分成兩層:
~/.claude/settings.json): 你自己所有專案通用。.claude/settings.json): 只有這個專案用,而且會跟著專案一起 commit,所以隊友拉下來也會有一樣的設定。另外還有一個 .claude/settings.local.json,一樣是放在專案裡,可是它是給你個人用的,不會 commit 進 Git,等一下第二個 Hook 我們就會用到它。
那看到這邊你可能會想說,這跟直接在 CLAUDE.md 寫規則有什麼不一樣?
大概差別在於「拜託你做」跟「強制你做」的差異吧。
不好懂?前面我們有介紹過 CLAUDE.md 給 AI 看的規則、憲法,每一次開啟新的對話時,它都會載入,但其實儘管說它是憲法、規範,但還是有大機率會被遺忘,尤其是 Context 太長的時候,所以 CLAUDE.md 比較偏向「拜託你做」的概念,AI 會盡量遵守,但不保證每次都記得。
但 Hook 則是建立在系統層級上的自動化機關,只要 AI 執行到特定行為、事件,就必定會觸發 Hook,完全不經過 AI 的判斷,所以它的強制性就比 CLAUDE.md 高上很多。
那常見哪些時候呢?最簡單的就是「改完檔案要排版」,這種每次都一定要做的事情,就很適合用 Hook 而沒有必要經過 AI 的判斷。

所以接下來我們將會來實際體驗兩個 Hook 的實作,一個是共享的 PostToolUse 排版 Hook,另一個是個人的 Stop 桌面通知 Hook。
Note
其實你會發現我一直反覆強調 Context 的重要性,不管你用的是哪個方案,甚至是打 API 的方式,AI 最昂貴的永遠都是 Token 以及 Context 的妥善使用。
接下來我們要來體驗一下 Hook 的威力,先從最簡單的排版開始。
首先這邊會需要先做一件事情,你要先替 money-note 專案建立一個 .claude/hooks 資料夾,而這個資料夾是用來放 Hook 腳本的。
接著我們要替這個專案安裝一個新套件,叫做 Prettier,它是一個程式碼排版工具,會依照固定的規則幫你整理縮排、引號、分號這些東西,讓專案裡的程式碼長得一致,所以一樣請你打開終端機並移動到這個專案下,接著輸入以下指令:
npm install -D prettier
當然,你也可以懶一點,輸入以下 Prompt 讓 AI 幫你安裝:
請在 money-note 專案安裝 Prettier,並把它加到 devDependencies 裡。
Note
如果你的專案已經有裝 Prettier,這一步跳過就好。
在貼程式碼之前,先說明一下這個 Hook 會如何運作:
settings.json 找有沒有掛在這個事件上的 Hook。這邊我也順手畫成流程圖:

所以我們要準備兩個東西:
接著請你在專案底下建立 .claude/hooks/format-edited-file.mjs 這個檔案,並貼上以下內容(當然你也可以直接把這段丟給 AI,請它幫你建立):
import { spawnSync } from 'node:child_process'
import { isAbsolute, relative, resolve } from 'node:path'
let raw = ''
for await (const chunk of process.stdin) raw += chunk
try {
const input = JSON.parse(raw)
const filePath = input.tool_input?.file_path
if (!filePath) process.exit(0)
const projectRoot = resolve(process.env.CLAUDE_PROJECT_DIR || process.cwd())
const target = isAbsolute(filePath) ? resolve(filePath) : resolve(projectRoot, filePath)
const pathFromRoot = relative(projectRoot, target)
if (pathFromRoot.startsWith('..') || isAbsolute(pathFromRoot)) process.exit(0)
const prettierBin = resolve(projectRoot, 'node_modules/prettier/bin/prettier.cjs')
const result = spawnSync(process.execPath, [prettierBin, '--write', '--ignore-unknown', target], {
cwd: projectRoot,
stdio: ['ignore', 'inherit', 'inherit'],
})
process.exit(result.status ?? 0)
} catch (error) {
console.error('格式化 Hook 無法讀取輸入:', error.message)
process.exit(1)
}
上面程式碼你不用太花時間看,基本上就只是做三件事情:
tool_input.file_path。所以接著我們就要來把 Hook 的腳本掛上去(.claude/settings.json),讓它在 PostToolUse 事件發生時自動執行,內容如下:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/format-edited-file.mjs\""
}
]
}
]
}
}
很短吧?這邊不用擔心,我也會一一解釋:
PostToolUse: 生命週期事件,也就是 工具執行完之後 觸發。matcher: 建立過濾條件,也就是只有 Edit 跟 Write 這兩個改檔工具跑完才觸發,其餘的讀取檔案 or 執行指令都不會觸發。command: 想要執行的指令,這邊是使用 Node 去跑剛剛那支腳本,${CLAUDE_PROJECT_DIR} 是 Claude Code 提供的變數,代表專案的根目錄。基本上儲存之後 Claude Code 會自己偵測到設定檔有變動,不過保險起見你也可以重開一次,確保它真的有把設定讀進來。
那我們該如何測試呢?很簡單,輸入以下 Prompt 給 AI:
請用 Write 工具建立 src/hook-test.js,裡面隨便寫一個小函式,縮排跟空格故意亂排、不要加分號,直接寫進去就好,不用檢查也不用排版。
你應該會看到它寫的很亂很糟:

但...實際上你打開 src/hook-test.js,你會發現排版已經被整理得整整齊齊了

Note
這邊 Prompt 我特別寫「用 Write 工具」是有原因的,因為 Hook 的matcher只會看 Edit 跟 Write 這兩個工具,如果 AI 是用 Bash 指令(像cat > 檔案)把檔案寫出來,那...Hook 就不會跑,所以才刻意指定 Write 工具。
接下來這個 Hook 算是連我個人都會使用的 Hook,畢竟我滿常丟一些需要跑比較久的任務給 AI,但跑的過程我總不可能一直盯著終端機看吧?!肯定會善用時間去做一些零碎且可以被中斷的事情,所以我就有針對 Hooks 寫一個 Stop Hook,當 AI 每次結束回應時,就跳一個桌面通知給我。
那...這個範例要特別提醒一下,這是專門於 macOS 的,所以建議你不要用在 Windows 或 Linux,因為它們的桌面通知指令不一樣,通常這類通知也比較偏向「個人」,所以會建議放在 .claude/settings.local.json,不要放在共享的 .claude/settings.json,不然隊友拉下來就會跳通知給他們 xD
所以請你建立 .claude/settings.local.json,貼上以下內容:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"money-note 的任務完成了\" with title \"Claude Code\"'"
}
]
}
]
}
}
Note
osascript是 macOS 的專屬指令,除此之外,Stop 沒有matcher可以設定,所以這邊直接寫hooks就可以囉。
這邊有件事情也要提醒一下,通常 .gitignore 並不會把 .claude/settings.local.json 排除在外,因為它是手動建立的,所以請你自己把它加進 .gitignore,避免不小心 commit 進 Git。
把上面 Hook 加入之後,我們就可以來試著觸發看看了,你可以隨便輸入個 Prompt 讓 AI 幫你改一個檔案,像是:
Hello!今天你好嗎?
不出意外的話,你就會得到一個 macOS 桌面通知,告訴你「money-note 的任務完成了」。

很簡單吧?Hook 非常的強大,我甚至還把它接到我的 Discord 頻道上呢。
那到這邊為止,也要同時提醒一件事情,儘管 Hook 非常強大,它的價值也建立於強制性上,但同時這也是一個風險。
什麼意思呢?當 CLAUDE.md 的規則有問題或者寫錯時,AI 可以自己判斷去修正、去繞過去,但如果你 Hook 出現錯誤,那麼每次觸發都會爆一次,而且它不會幫你判斷。
所以基本上我會建議你跟 AI 討論之後,請 AI 幫你寫好 Hook 腳本,接著在你自己電腦的終端機先測試過,確認它能正常運作之後,再掛上去,這樣可以大幅度減少一些不必要的問題。
像我們這次的腳本就可以這樣測,丟一份沒有檔案路徑的 JSON 給它,正常的話它會安靜地結束,不會有任何錯誤:
printf '%s' '{"tool_input":{}}' | node .claude/hooks/format-edited-file.mjs
那這章節也介紹差不多了,也一樣總結一下吧!
settings.json 的 hooks 區塊,共享的放專案的 .claude/settings.json 會跟著 commit,個人的(像桌面通知)放 .claude/settings.local.json,記得自己加進 .gitignore。matcher 決定哪些工具會觸發,這次只盯 Edit 跟 Write,所以 AI 用 Bash 寫檔是不會觸發的。那明天我們就來聊測試,Day 14 說過「錯了不痛可以先放生」的那些 code,該有人幫你盯著了。
希望這一篇有讓你對 Hook 有更進一步的認識哩~
我們明天 Day 23 見囉~