Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦

Search for a command to run...

No comments yet. Be the first to comment.
書本連結 前言 會挑這本書看的人,應該都是關心自己負責專案的軟體架構的人。不只希望開發的軟體能滿足客戶的明確需求,也希望能滿足可維護性(maintainability)的隱性需求,以及自己對結構與美觀習慣的要求。 要能滿足上述這些要求很難,因為專案通常不會按照計畫進行。可能變因有 deadline,最後的 API 與承諾的不同,又或者我們的設計無法很好的貼合需求的變化所需。。因此完美的架構只有在一

我們的 AIOps agent 有個很具體的問題:它會捏造 trace ID。 當 on-call 工程師問「payment service 有沒有 error trace?」,agent 有時會信心滿滿地回答「是的,見 trace a1b2c3d4...」——但這串 ID 根本不存在 Tempo 裡。工程師點進去,404。壞的不只是使用者體驗,而是這讓整個 RCA 結論失去可信度。 另一個問題是

o11y-bench 深入剖析:讓 AI 真正面對 on-call 現場 從任務設計、合成環境、Agent 架構、評分機制到報告輸出,逐一解析這個開放 benchmark 的每個組件——以及 Gemini 3 Flash Preview 的完整實測結果 先說清楚這在解決什麼問題 目前多數 LLM benchmark 測的是「知識」:模型知不知道 PromQL 的語法,知不知道什麼是 p99

臥龍神算奇術完全兵書:從兵法原理到實戰,徹底搞懂奇術機制

稟告主公:此乃司馬懿進呈之兵書,詳解如何以 OpenTelemetry 陣法,令臥龍神算之一舉一動盡在掌握,知糧草消耗、察兵器效能、辨戰報異常,使主公運籌帷幄於大帳之中。 為何需要斥候情報? 司馬懿稟告主公: 臥龍神算(Claude Code)乃當世利器,然若無斥候回報,主公便如蒙眼行軍——兵器耗損幾何、糧草消費幾許、哪路斥候出了差錯,一概不知。臣以為,此乃兵家大忌。 無情報之弊,有四: 軍

在現代 AI 輔助開發中,我們不僅需要 AI 寫程式,更需要它懂規則、記性好,並且能自動處理那些繁瑣的雜事。透過 Claude Code Hooks 機制,我們可以介入 AI 的思考與執行迴圈,實現真正的「人機協作自動化」。
在預設狀態下,Claude Code 雖然強大,但它是「被動」且「無狀態」的,這導致了開發者常遇到以下痛點:
記憶重置 (Session Amnesia):
痛點:每次重啟終端機,AI 就像失憶一樣。
解法:你需要一個機制,在 SessionStart 時自動把「上一集的劇情(Session Log)」灌輸給它。
程式碼品質不一 (Inconsistent Quality):
痛點:AI 寫出的 Go 程式碼可能忘了 gofmt,或者留下了 fmt.Println 除錯訊息。
解法:你需要一個「糾察隊」,在 PostToolUse(工具用完後)自動執行格式化與檢查。
危險操作 (Safety Risks):
痛點:AI 有時會過度自信,想直接 git push 到主分支。
解法:你需要在 PreToolUse(工具執行前)設下攔截點,強制顯示警告。
上下文丟失 (Context Drift):
痛點:對話太長時,重要資訊被壓縮丟棄。
解法:利用 PreCompact 在壓縮發生前,將關鍵狀態寫入硬碟。
要掌握 Hooks,必須理解這張生命週期圖。這不僅是流程,更是我們可以「插入程式碼」的機會點:

| Hook 事件 | 觸發時機 | 應用場景 |
SessionStart | Claude Code 啟動時 | 載入上次進度、顯示專案狀態 |
SessionEnd | 會話結束時 | 保存工作進度、建立 session 記錄 |
PreToolUse | 執行工具前 | 安全檢查、阻擋危險操作 |
PostToolUse | 執行工具後 | 程式碼格式化、品質檢查 |
Stop | AI 完成回應時 | 檢查未提交的 debug 程式碼 |
PreCompact | Context 壓縮前 | 保存重要狀態、記錄壓縮事件 |
我們可以將其劃分為三個戰略區域:
SessionStart:這是「載入記憶」的時刻。你的腳本 session-start.js 在這裡執行,負責掃描 .claude/sessions/ 下的 .tmp 檔案,告訴 AI 上次工作到哪裡。
SessionEnd:這是「存檔」的時刻。session-end.js 會將當前的狀態快照保存下來,供下次使用。
這是圖中橙色虛線框起來的部分,也是 AI 實際工作的地方。
PreToolUse (攔截層):在 AI 真正執行 Bash 或 Edit 之前。這是防止錯誤的最佳時機(例如阻擋 git push)。
PostToolUse (修正層):在 AI 修改完檔案後。你的腳本可以在這裡自動執行 gofmt,或是檢查有沒有遺留的 fmt.Print。
PreCompact:當 Token 即將爆滿時,系統會觸發壓縮。利用 pre-compact.js 記錄這一事件,防止重要資訊在壓縮中「無聲消失」。Hooks 配置在 .claude/settings.local.json 中:
{
"hooks": {
"HookEvent": [
{
"matcher": "條件表達式",
"hooks": [
{
"type": "command",
"command": "要執行的指令"
}
]
}
]
}
}
"*" - 匹配所有情況
tool == "Bash" - 匹配特定工具
tool == "Edit" && tool_input.file_path matches "\\.go$" - 組合條件
!(tool_input.file_path matches "README\\.md") - 排除條件
結合 Go 專案範例,這套配置實現了以下具體功能:
SessionStart)你的配置不再只是依賴單一的 CLAUDE.md,而是引入了時間序列的 Session Log。
行為:腳本會檢查 go.mod 確認這是 Go 專案,並自動尋找最近修改過的 sessions/*.tmp 檔案。
優勢:AI 一啟動就知道專案類型(Go Module)以及上次具體的工作內容,實現「無縫熱啟動」。
配置範例:
{
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node /path/to/project/.claude/scripts/hooks/session-start.js"
}
]
}
]
}
session-start.js:
#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
function main() {
const sessionsDir = path.join(process.env.HOME, '.claude', 'sessions');
// 檢查是否有最近的 session 記錄
if (fs.existsSync(sessionsDir)) {
const files = fs.readdirSync(sessionsDir)
.filter(f => f.endsWith('.tmp'))
.sort().reverse();
if (files.length > 0) {
console.error(`[SessionStart] Found ${files.length} recent session(s)`);
console.error(`[SessionStart] Latest: ${files[0]}`);
}
}
// Go 專案檢測
if (fs.existsSync('go.mod')) {
const content = fs.readFileSync('go.mod', 'utf8');
const match = content.match(/^module\s+(.+)$/m);
if (match) {
console.error(`[SessionStart] Go project: ${match[1]}`);
}
}
process.exit(0);
}
main();
PostToolUse)這是這套配置最精彩的部分—— 自動修正 (Auto-Correction)。
行為:當監測到 Edit 工具修改了 .go 檔案後,Hooks 會自動觸發:
gofmt -w "file_path"
同時,若發現檔案內含有 fmt.Print,會透過 console.error 警告開發者。
優勢:即使 AI 生成的程式碼格式混亂,寫入硬碟的那一刻也會被強制修正為標準格式。這大幅減少了 code review 的負擔。
配置範例:
{
"PostToolUse": [
{
"matcher": "tool == \"Edit\" && tool_input.file_path matches \"\\\\.go$\"",
"hooks": [
{
"type": "command",
"command": "node -e \"const{execSync}=require('child_process');const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path;if(p&&fs.existsSync(p)){try{execSync('gofmt -w \\\"'+p+'\\\"',{stdio:['pipe','pipe','pipe']})}catch(e){console.error('[Hook] gofmt failed: '+e.message)}}console.log(d)})\""
}
]
},
{
"matcher": "tool == \"Edit\" && tool_input.file_path matches \"\\\\.go$\"",
"hooks": [
{
"type": "command",
"command": "node -e \"const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path;if(p&&fs.existsSync(p)){const c=fs.readFileSync(p,'utf8');if(/fmt\\\\.Print(ln|f)?\\\\(/.test(c)){console.error('[Hook] WARNING: fmt.Print found in '+p);console.error('[Hook] Consider using proper logging instead')}}console.log(d)})\""
}
]
},
{
"matcher": "tool == \"Bash\"",
"hooks": [
{
"type": "command",
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const cmd=i.tool_input?.command||'';if(/gh pr create/.test(cmd)){const out=i.tool_output?.output||'';const m=out.match(/https:\\\\/\\\\/github.com\\\\/[^/]+\\\\/[^/]+\\\\/pull\\\\/\\\\d+/);if(m){console.error('[Hook] PR created: '+m[0])}}console.log(d)})\""
}
]
}
]
}
PreToolUse)行為:當 AI 試圖執行 git push 時,Hooks 會攔截並輸出:
[Hook] Review changes before push... Consider: git diff HEAD~1
優勢:增加了一道「冷靜期」,防止 AI 在未經人工確認的情況下將錯誤程式碼推送到遠端倉庫。
配置範例:
{
"PreToolUse": [
{
"matcher": "tool == \"Bash\" && tool_input.command matches \"git push\"",
"hooks": [
{
"type": "command",
"command": "node -e \"console.error('[Hook] Review changes before push...');console.error('[Hook] Consider: git diff HEAD~1, git log --oneline -5')\""
}
]
},
{
"matcher": "tool == \"Write\" && tool_input.file_path matches \"\\\\.(md|txt)$\" && !(tool_input.file_path matches \"README\\\\.md|CLAUDE\\\\.md\")",
"hooks": [
{
"type": "command",
"command": "node -e \"console.error('[Hook] WARNING: Creating documentation file');console.error('[Hook] Consider using CONTEXT.md for session notes')\""
}
]
}
]
}
Stop)配置範例:
{
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node -e \"const{execSync}=require('child_process');const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{execSync('git rev-parse --git-dir',{stdio:'pipe'})}catch{console.log(d);process.exit(0)}try{const files=execSync('git diff --name-only HEAD',{encoding:'utf8',stdio:['pipe','pipe','pipe']}).split('\\\\n').filter(f=>/\\\\.go$/.test(f)&&fs.existsSync(f));let hasDebug=false;for(const f of files){const content=fs.readFileSync(f,'utf8');if(/fmt\\\\.Print(ln|f)?\\\\(/.test(content)){console.error('[Hook] WARNING: fmt.Print found in '+f);hasDebug=true}}if(hasDebug)console.error('[Hook] Remove debug prints before committing')}catch(e){}console.log(d)})\""
}
]
}
]
}
SessionEnd)配置範例:
{
"SessionEnd": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node /path/to/project/.claude/scripts/hooks/session-end.js"
}
]
}
]
}
session-end.js:
#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
function main() {
const sessionsDir = path.join(process.env.HOME, '.claude', 'sessions');
const today = new Date().toISOString().split('T')[0];
const sessionFile = path.join(sessionsDir, `${today}-session.tmp`);
if (!fs.existsSync(sessionsDir)) {
fs.mkdirSync(sessionsDir, { recursive: true });
}
const time = new Date().toTimeString().slice(0, 5);
if (fs.existsSync(sessionFile)) {
let content = fs.readFileSync(sessionFile, 'utf8');
content = content.replace(/\*\*Last Updated:\*\*.*/, `**Last Updated:** ${time}`);
fs.writeFileSync(sessionFile, content);
console.error(`[SessionEnd] Updated session: ${today}-session.tmp`);
} else {
const template = `# Session: ${today}
**Started:** ${time}
**Last Updated:** ${time}
## Completed
- [ ]
## In Progress
- [ ]
## Notes for Next Session
-
`;
fs.writeFileSync(sessionFile, template);
console.error(`[SessionEnd] Created session: ${today}-session.tmp`);
}
process.exit(0);
}
main();
PreCompact)配置範例:
{
"PreCompact": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node /path/to/project/.claude/scripts/hooks/pre-compact.js"
}
]
}
]
}
pre-compact.js:
#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
function main() {
const sessionsDir = path.join(process.env.HOME, '.claude', 'sessions');
const logFile = path.join(sessionsDir, 'compaction-log.txt');
if (!fs.existsSync(sessionsDir)) {
fs.mkdirSync(sessionsDir, { recursive: true });
}
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
fs.appendFileSync(logFile, `[${timestamp}] Context compaction triggered\n`);
console.error('[PreCompact] State preserved before compaction');
process.exit(0);
}
main();
建議的專案 hooks 結構:
.claude/
├── settings.local.json # Hooks 配置
├── scripts/
│ ├── hooks/
│ │ ├── session-start.js
│ │ ├── session-end.js
│ │ └── pre-compact.js
│ └── lib/
│ └── utils.js # 共用工具函式
└── skills/ # Claude Code skills
Claude Code 本質上是一個 「事件驅動的執行環境 (Event-Driven Execution Environment)」。
彌補 LLM 的缺陷:LLM 擅長生成,但不擅長「守紀律」和「記狀態」。Hooks 透過確定性的程式碼(Node.js/Shell)來彌補機率性的 AI 模型。
標準輸入/輸出的管道設計: 注意到腳本中使用了 process.stdin 和 process.stdout 嗎?
process.stdin.on('data', c => d += c); // 接收 Claude 的數據
console.log(d); // 必須把數據傳下去,否則流程會斷掉
這設計讓 Hooks 成為類似 Linux Pipe 的過濾器,可以在不打斷 AI 思路的前提下,對資料進行「偷看」、「修改」或「阻擋」。
Hook 必須輸出 stdin 資料:對於需要處理輸入的 hooks,必須在最後輸出 console.log(d) 將原始資料傳遞下去
使用 stderr 顯示訊息:console.error() 用於顯示給使用者的訊息,console.log() 用於傳遞資料
避免阻塞:Hook 腳本應快速執行,避免耗時操作
錯誤處理:即使發生錯誤也應 process.exit(0),避免阻斷 Claude Code 流程
路徑處理:使用絕對路徑或相對於專案根目錄的路徑
一旦部署這套 .claude/settings.local.json,你的開發體驗將發生如下質變:
| 開發情境 | 觸發設定 (Hook) | 系統自動化行為 | 開發者獲得的好處 |
| 開啟專案 | SessionStart | 自動讀取 sessions/2026-01-24.tmp 並分析 go.mod。 | 秒進狀態:不用再打字解釋「這是 Go 專案,上次做到哪」。 |
| AI 寫完 Code | PostToolUse | 背景靜默執行 gofmt。 | 格式完美:檔案永遠符合 Go Standard,不會有縮排錯誤。 |
| AI 忘記刪 Log | Stop/PostToolUse | 掃描並紅字警告:WARNING: fmt.Print found。 | 保持潔淨:防止 Debug 代碼污染生產環境。 |
| 準備提交 PR | PreToolUse | 攔截 git push 並建議先 git diff。 | 安全防護:避免意外將實驗性代碼推上線。 |
| 關閉終端 | SessionEnd | 將當前進度寫入 *-session.tmp。 | 進度固化:確保今天的上下文能準確傳遞給明天的自己。 |
這套配置將 記憶持久化概念 進一步細化為針對 Go 語言特性的自動化工作流。它不僅解決了「失憶」問題,更透過 gofmt 和安全檢查,讓 AI 成為了一個「守紀律」的初級工程師,而不僅僅是一個聊天機器人。