Skip to main content

Command Palette

Search for a command to run...

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

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

在現代 AI 輔助開發中,我們不僅需要 AI 寫程式,更需要它懂規則、記性好,並且能自動處理那些繁瑣的雜事。透過 Claude Code Hooks 機制,我們可以介入 AI 的思考與執行迴圈,實現真正的「人機協作自動化」。


一、 動機與痛點:為什麼你需要介入 AI 的生命週期?

在預設狀態下,Claude Code 雖然強大,但它是「被動」且「無狀態」的,這導致了開發者常遇到以下痛點:

  1. 記憶重置 (Session Amnesia)

    • 痛點:每次重啟終端機,AI 就像失憶一樣。

    • 解法:你需要一個機制,在 SessionStart 時自動把「上一集的劇情(Session Log)」灌輸給它。

  2. 程式碼品質不一 (Inconsistent Quality)

    • 痛點:AI 寫出的 Go 程式碼可能忘了 gofmt,或者留下了 fmt.Println 除錯訊息。

    • 解法:你需要一個「糾察隊」,在 PostToolUse(工具用完後)自動執行格式化與檢查。

  3. 危險操作 (Safety Risks)

    • 痛點:AI 有時會過度自信,想直接 git push 到主分支。

    • 解法:你需要在 PreToolUse(工具執行前)設下攔截點,強制顯示警告。

  4. 上下文丟失 (Context Drift)

    • 痛點:對話太長時,重要資訊被壓縮丟棄。

    • 解法:利用 PreCompact 在壓縮發生前,將關鍵狀態寫入硬碟。


二、 核心機制:生命週期圖解 (The Lifecycle)

要掌握 Hooks,必須理解這張生命週期圖。這不僅是流程,更是我們可以「插入程式碼」的機會點:

Hook lifecycle diagram showing the sequence of hooks from SessionStart through the agentic loop to SessionEnd

Hook 事件觸發時機應用場景
SessionStartClaude Code 啟動時載入上次進度、顯示專案狀態
SessionEnd會話結束時保存工作進度、建立 session 記錄
PreToolUse執行工具前安全檢查、阻擋危險操作
PostToolUse執行工具後程式碼格式化、品質檢查
StopAI 完成回應時檢查未提交的 debug 程式碼
PreCompactContext 壓縮前保存重要狀態、記錄壓縮事件

我們可以將其劃分為三個戰略區域:

1. 啟動與結束區 (Session Management)

  • SessionStart:這是「載入記憶」的時刻。你的腳本 session-start.js 在這裡執行,負責掃描 .claude/sessions/ 下的 .tmp 檔案,告訴 AI 上次工作到哪裡。

  • SessionEnd:這是「存檔」的時刻。session-end.js 會將當前的狀態快照保存下來,供下次使用。

2. 代理循環區 (The Agentic Loop) - 自動化的核心

這是圖中橙色虛線框起來的部分,也是 AI 實際工作的地方。

  • PreToolUse (攔截層):在 AI 真正執行 BashEdit 之前。這是防止錯誤的最佳時機(例如阻擋 git push)。

  • PostToolUse (修正層):在 AI 修改完檔案後。你的腳本可以在這裡自動執行 gofmt,或是檢查有沒有遺留的 fmt.Print

3. 維護區 (Maintenance)

  • PreCompact:當 Token 即將爆滿時,系統會觸發壓縮。利用 pre-compact.js 記錄這一事件,防止重要資訊在壓縮中「無聲消失」。

三、 配置結構與語法

Hooks 配置在 .claude/settings.local.json 中:

{
  "hooks": {
    "HookEvent": [
      {
        "matcher": "條件表達式",
        "hooks": [
          {
            "type": "command",
            "command": "要執行的指令"
          }
        ]
      }
    ]
  }
}

Matcher 語法

  • "*" - 匹配所有情況

  • tool == "Bash" - 匹配特定工具

  • tool == "Edit" && tool_input.file_path matches "\\.go$" - 組合條件

  • !(tool_input.file_path matches "README\\.md") - 排除條件


四、 實戰配置解析:你的腳本做了什麼?

結合 Go 專案範例,這套配置實現了以下具體功能:

1. 智慧型記憶掛載 (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();

2. 強制性程式碼規範 (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)})\""
        }
      ]
    }
  ]
}

3. 危險操作防護網 (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')\""
        }
      ]
    }
  ]
}

4. 提交前最後檢查 (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)})\""
        }
      ]
    }
  ]
}

5. Session 結束存檔 (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();

6. 壓縮前狀態保存 (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 仰賴這些設定?

Claude Code 本質上是一個 「事件驅動的執行環境 (Event-Driven Execution Environment)」

  1. 彌補 LLM 的缺陷:LLM 擅長生成,但不擅長「守紀律」和「記狀態」。Hooks 透過確定性的程式碼(Node.js/Shell)來彌補機率性的 AI 模型。

  2. 標準輸入/輸出的管道設計: 注意到腳本中使用了 process.stdinprocess.stdout 嗎?

     process.stdin.on('data', c => d += c); // 接收 Claude 的數據
     console.log(d); // 必須把數據傳下去,否則流程會斷掉
    

    這設計讓 Hooks 成為類似 Linux Pipe 的過濾器,可以在不打斷 AI 思路的前提下,對資料進行「偷看」、「修改」或「阻擋」。


七、 注意事項

  1. Hook 必須輸出 stdin 資料:對於需要處理輸入的 hooks,必須在最後輸出 console.log(d) 將原始資料傳遞下去

  2. 使用 stderr 顯示訊息console.error() 用於顯示給使用者的訊息,console.log() 用於傳遞資料

  3. 避免阻塞:Hook 腳本應快速執行,避免耗時操作

  4. 錯誤處理:即使發生錯誤也應 process.exit(0),避免阻斷 Claude Code 流程

  5. 路徑處理:使用絕對路徑或相對於專案根目錄的路徑


八、 配置後帶來的行為變革

一旦部署這套 .claude/settings.local.json,你的開發體驗將發生如下質變:

開發情境觸發設定 (Hook)系統自動化行為開發者獲得的好處
開啟專案SessionStart自動讀取 sessions/2026-01-24.tmp 並分析 go.mod秒進狀態:不用再打字解釋「這是 Go 專案,上次做到哪」。
AI 寫完 CodePostToolUse背景靜默執行 gofmt格式完美:檔案永遠符合 Go Standard,不會有縮排錯誤。
AI 忘記刪 LogStop/PostToolUse掃描並紅字警告:WARNING: fmt.Print found保持潔淨:防止 Debug 代碼污染生產環境。
準備提交 PRPreToolUse攔截 git push 並建議先 git diff安全防護:避免意外將實驗性代碼推上線。
關閉終端SessionEnd將當前進度寫入 *-session.tmp進度固化:確保今天的上下文能準確傳遞給明天的自己。

總結

這套配置將 記憶持久化概念 進一步細化為針對 Go 語言特性的自動化工作流。它不僅解決了「失憶」問題,更透過 gofmt 和安全檢查,讓 AI 成為了一個「守紀律」的初級工程師,而不僅僅是一個聊天機器人。


參考資源

457 views

More from this blog

Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

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

Feb 19, 202610 min read163
Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

工程師的 Claude Code 實戰指南:從零開始到高效開發

工程師的 Claude Code 實戰指南:從零開始到高效開發 本文整合 Anthropic 官方 Best Practices 與社群實戰 Tips,帶你由淺入深掌握 Claude Code。 什麼是 Claude Code?為什麼值得學? 如果你還在用「複製程式碼貼到 ChatGPT,再複製答案貼回去」的工作流程,Claude Code 會讓你大開眼界。 Claude Code 是 Anthropic 推出的命令列工具,它直接活在你的 terminal 裡,能夠讀懂你的整個 codeb...

Feb 18, 20265 min read75
工程師的 Claude Code 實戰指南:從零開始到高效開發

System Design Interview Ch 12 Digital Wallet

確立問題與設計範疇 角色對話內容 面試者我們應該只關注兩個數位錢包之間的餘額轉帳操作嗎?我們是否需要擔心其他功能? 面試官讓我們只關注餘額轉帳操作。 面試者該系統需要支援多少 TPS(每秒交易次數)? 面試官讓我們假設是 1,000,000 TPS (每秒 100 萬次交易)。 面試者數位錢包對正確性有嚴格的要求。我們可以假設事務保證 就足夠了嗎? 面試官聽起來不錯。 面試者我們需要證明正確性嗎? 面試官這是一個很好的問題。正確性(Correctness)通常只有在交...

Feb 2, 202610 min read190
System Design Interview Ch 12 Digital Wallet
M

MicroFIRE

71 posts