# 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](https://mintcdn.com/claude-code/z2YM37Ycg6eMbID3/images/hooks-lifecycle.png?fit=max&auto=format&n=z2YM37Ycg6eMbID3&q=85&s=5c25fedbc3db6f8882af50c3cc478c32 align="left")

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

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

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

* `SessionStart`：這是「載入記憶」的時刻。你的腳本 `session-start.js` 在這裡執行，負責掃描 `.claude/sessions/` 下的 `.tmp` 檔案，告訴 AI 上次工作到哪裡。
    
* `SessionEnd`：這是「存檔」的時刻。`session-end.js` 會將當前的狀態快照保存下來，供下次使用。
    

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

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

* `PreToolUse` (攔截層)：在 AI 真正執行 `Bash` 或 `Edit` 之前。這是防止錯誤的最佳時機（例如阻擋 `git push`）。
    
* `PostToolUse` (修正層)：在 AI 修改完檔案後。你的腳本可以在這裡自動執行 `gofmt`，或是檢查有沒有遺留的 `fmt.Print`。
    

### 3\. 維護區 (Maintenance)

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

---

## 三、 配置結構與語法

Hooks 配置在 `.claude/settings.local.json` 中：

```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）以及上次具體的工作內容，實現「無縫熱啟動」。
    

**配置範例：**

```json
{
  "SessionStart": [
    {
      "matcher": "*",
      "hooks": [
        {
          "type": "command",
          "command": "node /path/to/project/.claude/scripts/hooks/session-start.js"
        }
      ]
    }
  ]
}
```

**session-start.js：**

```javascript
#!/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 會自動觸發：
    
    ```bash
    gofmt -w "file_path"
    ```
    
    同時，若發現檔案內含有 `fmt.Print`，會透過 `console.error` 警告開發者。
    
* **優勢**：即使 AI 生成的程式碼格式混亂，寫入硬碟的那一刻也會被強制修正為標準格式。這大幅減少了 code review 的負擔。
    

**配置範例：**

```json
{
  "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 在未經人工確認的情況下將錯誤程式碼推送到遠端倉庫。
    

**配置範例：**

```json
{
  "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`)

**配置範例：**

```json
{
  "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`)

**配置範例：**

```json
{
  "SessionEnd": [
    {
      "matcher": "*",
      "hooks": [
        {
          "type": "command",
          "command": "node /path/to/project/.claude/scripts/hooks/session-end.js"
        }
      ]
    }
  ]
}
```

**session-end.js：**

```javascript
#!/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`)

**配置範例：**

```json
{
  "PreCompact": [
    {
      "matcher": "*",
      "hooks": [
        {
          "type": "command",
          "command": "node /path/to/project/.claude/scripts/hooks/pre-compact.js"
        }
      ]
    }
  ]
}
```

**pre-compact.js：**

```javascript
#!/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 結構：

```sql
.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.stdin` 和 `process.stdout` 嗎？
    
    ```javascript
    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 寫完 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 成為了一個「守紀律」的初級工程師，而不僅僅是一個聊天機器人。

---

## 參考資源

* [Claude Code Hooks 官方文件](https://code.claude.com/docs/en/hooks)
