# Claude Code Hooks 2026：逐步設定與 7 個可直接使用的範例

> 7 個可直接使用的 .claude/settings.json hooks，複製貼上即可：保護 .env、prettier、禁止 force-push 至 main、檢查 em dash，以及開發伺服器。4 種處理器類型和 31 個事件。

Source: https://ailoot.ai/blog/zh-hant-tw/claude-code-hooks/
Canonical: https://ailoot.ai/blog/zh-hant-tw/claude-code-hooks/
Language: zh-Hant-TW
Publisher: Ailoot (https://ailoot.ai/)
Translation: guide-121
Revision: sha256-38b639f19601c05c7899666a8e25295f3b824fb0b6b74e4b71480e433c30c8cd


Claude Code 中的 hooks 是 shell 命令、HTTP 呼叫或簡短的 LLM 檢查，會在 Claude 執行動作前後自動執行。它們在特定工具事件增加檢查、編輯後格式化及啟動時注入背景。匹配的 PreToolUse 可拒絕該呼叫；regex 範例只是便利防護，不是涵蓋每種檔案或 Git 路徑的安全邊界。接下來會介紹 4 種處理器類型、31 個生命週期事件、「何時該把規則放進 hook」的測試、7 個可複製到 `.claude/settings.json` 的 hooks，以及我親身遇過的 3 個陷阱。



[Ailoot 文件：API、金鑰與設定](https://ailoot.ai/docs/)。

## Claude Code 中的 hooks 是什麼？

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

重點摘要

Hook 是 Claude Code 針對特定生命週期事件執行的 shell 命令、HTTP 呼叫、簡短的 LLM 提示或子代理。與模型可能忽略或忘記的 `CLAUDE.md` 指示不同，hook 每次都會以相同方式執行：它是程式碼，不是建議。

</div>

有半年時間，我都把「絕對不要直接編輯 `.env`」這條規則放在 `CLAUDE.md` 裡。Claude 每個月都會違反一次。不是故意的；只是在漫長的工作階段結束前忘了這條規則。有一次，`.env` 裡多了一個 token；另一次，同名變數覆蓋了正在使用的變數。

我把這條規則移到 hook 裡，讓它不可能被忘記。匹配的直接編輯工具提供 `.env` 路徑時，hook 會拒絕該呼叫；Bash 重導向、腳本、MCP 及其他路徑需另外控制，不代表檔案不可變。

<figure>
<blockquote>
<p>Hooks 是由你定義的 shell 命令、HTTP 端點或 LLM 提示，會在 Claude Code 生命週期的特定時點自動執行。</p>
</blockquote>
<figcaption>— Anthropic, https://code.claude.com/docs/en/hooks</figcaption>
</figure>

換句話說，hook 是你設定、讓 Claude Code 在特定事件發生時執行的命令。每次檔案編輯、Bash 啟動、工作階段開始或子代理完成，都是可以掛上 hook 的事件。

對 vibe coder 來說，這很重要，因為你可以：

- 保護含有機密的檔案，避免遭到意外修改。
- 每次產生程式碼後自動執行 prettier/eslint。
- 未經明確許可，禁止對 main 執行 `git push --force`。
- 在每個工作階段開始時注入最新的脈絡。
- 記錄 Claude 的操作以供稽核。

CLAUDE.md 負責脈絡。Hooks 負責**動作**。兩者是不同的東西，把它們混為一談是初學者最常犯的錯誤。

## Hooks 與 CLAUDE.md、skills 有什麼不同？

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

重點摘要

`CLAUDE.md` 是模型在工作階段開始時讀取並嘗試遵循的文字。Hook 是 Claude Code 程序在事件發生時執行的 shell 命令，不依賴模型。Skill 是一組有名稱的指示，Claude 可以透過 Skill 工具呼叫，就像函式一樣。三者分別適用於不同類型的任務。

</div>

Anthropic 的文件明確說明了 `CLAUDE.md` 和 hooks 的差異：

<figure>
<blockquote>
<p>CLAUDE.md 的內容是在系統提示之後，以使用者訊息的形式傳送，而不是系統提示的一部分。Claude 會閱讀並嘗試遵循，但無法保證會嚴格遵守。</p>
</blockquote>
<figcaption>— Anthropic, memory docs, https://code.claude.com/docs/en/memory</figcaption>
</figure>

換句話說，`CLAUDE.md` 的內容會以使用者傳來的第一則訊息送達 Claude，而不是系統指示。模型會閱讀並嘗試遵循，但沒有任何保證，尤其是在漫長工作階段的後段，脈絡逐漸膨脹、較早的指示被擠出時。

Hook 的運作方式不同。它位於 `settings.json`，不依賴模型，而是由 Claude Code 程序在特定事件發生時執行，例如 `PreToolUse`、`PostToolUse`、`SessionStart` 等。命令會在 Claude 收到結果前執行，並能實際阻擋動作（exit 2），或在編輯後修改檔案（exit 0）。

比較如下：

| 功能 | CLAUDE.md | Hook | Skill |
|----|----|----|----|
| 存放位置 | 專案根目錄中的 `CLAUDE.md` | `.claude/settings.json` | `.claude/skills/` 資料夾 |
| 執行者 | 模型將其視為使用者訊息的一部分來閱讀 | Claude Code 程序在事件發生時執行 | 模型透過 Skill 工具執行 |
| 執行保證 | 無（盡力而為） | 設定且匹配的 command hook 執行程式，失敗與結束行為依事件而異 | 透過模型呼叫（有授權，但不是 shell 層級） |
| 用途 | 專案脈絡、語氣、風格、路由 | 限制、格式化、檢查特定檔案 | 帶有參數的複雜多步驟程序 |
| 執行時機 | 工作階段中的每則模型訊息 | 生命週期事件發生時 | 由模型呼叫時 |
| 能否阻擋動作？ | 否 | 可以（exit 2） | 否 |

完整的 `CLAUDE.md` 範本，以及維護這個檔案的 6 條規則，請參閱指南[2026 年如何設定 CLAUDE.md：可直接使用的範本與 6 條規則](https://ailoot.ai/blog/zh-hant-tw/claude-md-template/)。如需了解以具名能力形式提供的 skills，請參閱[Claude Code Skills：如何建立自己的技能庫](https://ailoot.ai/blog/zh-hant-tw/claude-code-skills-library/)。

Hook 是最低層級：沒有模型、沒有解讀，只有 shell。

Hooks 是[脈絡工程](https://ailoot.ai/blog/zh-hant-tw/context-engineering/)工具之一，與 Plan Mode 和 /compact 並列。



## 4 種 hook 類型：Command、HTTP、Prompt、Agent——該選哪一種？

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

重點摘要

Claude Code 有 4 種處理器類型。Command 是最常見的 shell script。HTTP 會向你的 URL 傳送 POST 請求，適合團隊與整合用途。Prompt 會呼叫簡短的 Haiku，進行語意檢查。Agent 會建立能存取工具的子代理，進行深入驗證。初學者使用 Command 和 Prompt 就夠了，它們涵蓋 90% 的任務。

</div>

| 類型 | 功能 | 適用情況 | 複雜度 |
|----|----|----|----|
| **Command** | 執行 shell 命令，從 stdin 接收 JSON，並以結束代碼和 stdout 回應 | 90% 的情況：檢查路徑、格式化、對內容執行 grep、lsof、git 操作 | 低 |
| **HTTP** | 向你的 URL 傳送含 JSON 主體的 POST 請求，並接收 JSON 回應 | 規則應由團隊/CI 負責時：集中驗證、共用狀態儀表板、遠端稽核 | 中（需要伺服器） |
| **Prompt** | 將簡短提示傳給 Haiku（預設），並取得語意評估的 yes/no 結果 | grep 不夠用時：「評估這段文字聽起來是否像企業公文」 | 低 |
| **Agent** | 建立可存取 Read/Grep/Glob 的子代理，執行多步驟檢查 | 深入驗證：「找出所有變更過的檔案，並確認是否有相對應的測試」 | 高 |

最常見的錯誤，是用 Command 做應該交給 Prompt 的事。舉例來說，「`.md` 檔案不要使用 em dash」是 grep 能處理的工作，因此 Command 很適合。但「不要用企業腔調寫作」涉及語意，需要 Prompt。

如果你才剛開始，請使用 Command。以下 7 個可直接使用的 hooks 全都是 Command。等你準備好了，再使用 HTTP/Prompt/Agent。

## settings.json 在哪裡？4 個設定層級是什麼？

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

重點摘要

Claude Code 會從 4 個位置讀取設定：企業（透過 MDM 部署）、使用者（`~/.claude/settings.json`）、專案（儲存庫根目錄的 `.claude/settings.json`），以及本機（`.claude/settings.local.json`，不納入 git）。4 個檔案都會在工作階段開始時合併，較低層級的設定優先。

</div>

| 層級 | 路徑 | 適用範圍 | 納入 git？ |
|----|----|----|----|
| 企業 | `/Library/Application Support/ClaudeCode/settings.json`（Mac）、`/etc/claude-code/settings.json`（Linux） | 透過 MDM 套用至整個團隊 | 否 |
| 使用者 | `~/.claude/settings.json` | 單一使用者的所有專案 | 否 |
| 專案 | 儲存庫根目錄中的 `.claude/settings.json` | 整個團隊的這個專案 | **是** |
| 本機 | `.claude/settings.local.json` | 僅限這個使用者的這個專案 | **否**（列在 `.gitignore`） |

我把下文介紹的 7 個 hooks 放在專案的 `.claude/settings.json`，因此複製儲存庫後便會自動包含，Claude 在新的視窗中也會遵循相同規則。個人的實驗性 hooks 則放在 `.claude/settings.local.json`，以免影響團隊。

4 個檔案都會在工作階段開始時合併。如果規則衝突，較低層級優先（本機優先於專案，專案優先於使用者）。

**建立第一個 hook 最快的方法**，是在 Claude Code 中直接使用互動式 `/hooks` 選單。從清單選擇事件、選擇 matcher（例如 `Bash` 或 `Edit`），然後貼上命令。Claude Code 會替你在 `settings.json` 寫入有效的 JSON。

## 31 個生命週期事件：初學者需要哪些？

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

重點摘要

Claude Code 目前有 31 個生命週期事件，分成 3 種頻率：每個工作階段一次（`SessionStart`/`SessionEnd`）、每個回合一次（`UserPromptSubmit`/`Stop`），以及每次工具呼叫時（`PreToolUse`/`PostToolUse`）。初學者需要 31 個事件中的 4 個：限制操作用 `PreToolUse`、自動編輯用 `PostToolUse`、脈絡用 `SessionStart`，最後檢查用 `Stop`。

</div>

<figure>
<blockquote>
<p>事件分成三種頻率：每個工作階段一次（SessionStart、SessionEnd）、每個回合一次（UserPromptSubmit、Stop、StopFailure），以及代理循環中的每次工具呼叫時（PreToolUse、PostToolUse）。</p>
</blockquote>
<figcaption>— Anthropic, hooks reference, https://code.claude.com/docs/en/hooks</figcaption>
</figure>

了解這三種頻率很重要，才能避免混淆：

1.  **每個工作階段一次** - `SessionStart`、`SessionEnd`、`Setup`。Claude Code 開啟和關閉時各執行一次。可用來初始化脈絡及清除暫存檔案。
2.  **每個回合一次** - `UserPromptSubmit`、`UserPromptExpansion`、`Stop`、`StopFailure`。你每次傳送訊息，以及模型每次停止時都會觸發。可用來記錄對話和檢查最終結果。
3.  **每次工具呼叫時** - `PreToolUse`、`PostToolUse`、`PostToolUseFailure`、`PermissionRequest`、`PermissionDenied`、`PostToolBatch`、`SubagentStart`、`SubagentStop`。每次編輯檔案、啟動 Bash、讀取或搜尋時都會觸發。這是最頻繁的一層，限制、格式化和驗證都在這裡執行。

完整的 31 個事件清單還包含非同步/反應式事件：`TeammateIdle`、`PreCompact`、`PostCompact`、`Elicitation`、`ElicitationResult`、`InstructionsLoaded`、`ConfigChange`、`CwdChanged`、`FileChanged`、`WorktreeCreate`、`WorktreeRemove`、`Notification`。這些適用於進階情境，現在先略過。

## 五個問題測試：何時該把規則放進 hook，何時留在 CLAUDE.md？

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

重點摘要

如果規則涉及動作（而非脈絡）、關乎安全/金錢/資料、可以用 grep/regex 檢查、違反後難以復原，而且 Claude 至少已經違反過一次，就應該放進 hook。如果這五項中回答「是」的不到三項，就留在 `CLAUDE.md`。我用這個測試檢查了自己的 80 行規則，並將 5 條移到 hooks。

</div>

對我來說，這就像法律，而不是建議。我把 `CLAUDE.md` 和 hook 之間的選擇，濃縮成簡單的五個問題。如果至少有三個答案是「是」，這條規則就放進 hook。不到三個，就留在文字檔裡。

**「何時該把規則放進 hook？」測試：**

1.  這條規則涉及的是動作，而不是脈絡嗎？（「不要執行 `rm -rf`」是動作；「我有一個 TypeScript 專案」是脈絡。）
2.  這條規則關乎安全、金錢或資料嗎？（「不要碰 `.env`」、「不要公開金鑰」。）
3.  可以用 grep、regex 或 shell 命令的結束代碼檢查嗎？（「編輯 `.md` 後，檢查是否有 em dash。」）
4.  如果違反這條規則，後果會很難復原嗎？（「對 main 執行 force push」很難復原；「忘了在句尾加句號」則很容易。）
5.  你是否已經看過 Claude 至少忘記這條規則一次？

**範例：「不要用 em dash，只用連字號。」** 涉及動作？是。關乎安全？否。可以用 grep 檢查？是。難以復原？否。看過違規？是。**5 項中有 3 項：放進 hook。**

**反例：「我使用的是 vibe coder，不是開發者；請不要用俚語寫作。」** 涉及動作？比較像脈絡。關乎安全？否。可以用 regex 檢查？太難。難以復原？否。看過違規？是。**5 項中有 1 項：留在 `CLAUDE.md`。**

我的規則中有 5 條通過測試，因此移到了 hooks。其餘 75% 留在文字檔裡。

## 7 個可直接複製的 hooks

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

重點摘要

以下是 7 個可直接用於 `.claude/settings.json` 的 hooks：阻擋編輯 `.env`、自動執行 prettier、禁止對 main 執行 force-push、檢查 em dash、在 `npm run dev` 前釋放連接埠、測試成功後自動提交，以及在工作階段開始時注入最新脈絡。把每個 hook 複製到檔案中，重新啟動 Claude Code，就能使用。

</div>

7 個 hooks 都放在專案根目錄的同一個 `.claude/settings.json` 檔案中。每個項目都有事件類型、工具篩選器（matcher）和 shell 命令。Exit 2 可在執行前拒絕匹配 PreToolUse；PostToolUse 中動作已發生，hook 只能回報，不能撤回。

各事件的 exit 2 行為不同，請依 [官方 hooks 參考](https://code.claude.com/docs/en/hooks) 查核；PreToolUse 能阻擋呼叫，PostToolUse 不會撤回已發生編輯。

`exit 0` 代表成功。`exit 1` 代表發生錯誤，但 Claude 會繼續（這是下文第一個反模式）。`exit 2` 依事件而異，可阻擋 PreToolUse，PostToolUse 只回報已發生動作的結果。

### Hook 1. 檢查直接編輯 `.env`（PreToolUse、exit 2）

封鎖直接編輯任何符合 `.env`、`.env.local`、`.env.production` 等名稱模式的檔案。

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '(^|/)\\.env(\\.|$)' && { echo 'Do not edit `.env` directly. Tell me which variable to add, and I’ll open the file myself.' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}
```

**功能：** 每次 `Edit`/`Write`/`MultiEdit` 執行前，它會取得檔案路徑，並用 grep 檢查是否符合 `.env` 模式。如果符合，就回傳 exit 2 和一則訊息。Claude 會看到 stderr 並告訴我：「我嘗試編輯 `.env`，但被 hook 擋下來了；告訴我該新增哪個變數。」

### Hook 2. 每次編輯程式碼後自動執行 prettier（PostToolUse、exit 0）

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(ts|tsx|js|jsx|json)$' && npx prettier --write \"$FILE\" 2>/dev/null & exit 0"
          }
        ]
      }
    ]
  }
}
```

**功能：** 每次 `Edit`/`Write` 後，它會取得路徑；若副檔名是 `.ts`/`.tsx`/`.js`/`.jsx`/`.json`，就用 `&` 在背景執行 prettier。因為這是格式化而非限制，所以永遠回傳 exit 0。即使 prettier 失敗也沒關係，檔案會維持原樣。

與 hook 1 的差異至關重要：PreToolUse + exit 2 會在動作**之前**強制封鎖。PostToolUse + exit 0 則會在動作**之後**進行柔性編輯。這是適用於不同任務的兩種模式。

### Hook 3. 封鎖對 main 分支執行 `git push --force`（PreToolUse、exit 2）

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE 'git[[:space:]]+push' && echo \"$CMD\" | grep -qE '(\\-\\-force|\\-\\-force-with-lease|[[:space:]]-f([[:space:]]|$))' && echo \"$CMD\" | grep -qE '([[:space:]/:])main([[:space:]/:]|$)' && { echo 'Force push to main is forbidden. Create a new branch or ask me to do it manually.' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}
```

**功能：** 每次啟動 Bash 時，都會檢查命令是否包含 `push`、`--force`/`--force-with-lease`/`-f`，以及 `main`。三個條件都符合時，就會顯示清楚的訊息並封鎖操作。

以前我把這條規則用全大寫寫在 `CLAUDE.md` 裡。Claude 仍然大約每兩週就會違反一次，尤其是我叫它「force push 來復原錯誤的 commit」時。Hook 只抓列出的字面指令，別名、腳本、隱含目的分支或其他 Git client 可繞過字串檢查。遠端應強制保護 main，並限制代理憑證。

### Hook 4. 封鎖文字檔中的 em dash（PostToolUse、exit 2）

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(md|mdx|tsx?)$' && grep -q $'\\xe2\\x80\\x94' \"$FILE\" && { echo \"File $FILE contains an em dash (U+2014). Replace it with a regular hyphen (U+002D).\" >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}
```

**功能：** PostToolUse 的 exit 2 在編輯後回報檢查失敗，不會還原或防止寫入。如果 Claude 編輯檔案後仍留有 em dash，hook 就會失敗、向 Claude 提供回饋，讓它再試一次。經過 2–3 個工作階段後，Claude 就不再插入這個字元。

請注意，grep 使用 `$'\xe2\x80\x94'` 專門搜尋 em dash 字元（U+2014）；這是 Bash 中以 UTF-8 位元組表示字元的寫法。如果你只寫 `grep -q '-'`，一般連字號也會被找到，導致每個檔案都執行檢查。一個看不見的字元就會破壞整套邏輯。

### Hook 5. 在執行 `npm run dev` 前釋放連接埠 3000（PreToolUse、exit 0）

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE '(npm|pnpm|yarn)[[:space:]]+run[[:space:]]+dev' && { lsof -ti:3000 | xargs -r kill -9 2>/dev/null; echo 'Port 3000 freed before launch' >&2; }; exit 0"
          }
        ]
      }
    ]
  }
}
```

**功能：** 每次執行 `npm run dev`（或 `pnpm`/`yarn`）前，都會終止占用連接埠 3000 的程序。因為這是準備工作而非限制，所以永遠回傳 exit 0。這能避免一種常見陷阱：Claude 看到「port in use」，以為是程式碼出問題而開始修程式碼，但其實只是舊伺服器仍在執行。

### Hook 6. 測試成功後自動提交（PostToolUse、exit 0）

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "INPUT=$(cat); CMD=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty') || exit 1; EXIT=$(printf '%s' \"$INPUT\" | jq -r '.tool_response.exit_code // empty') || exit 1; case \"$CMD\" in 'npm test'|'pnpm test'|'yarn test') ;; *) exit 0 ;; esac; [ \"$EXIT\" = \"0\" ] || exit 0; git diff --cached --quiet || { echo 'Existing staged changes: review manually' >&2; exit 0; }; git add -- src/orders.ts tests/orders.test.ts || exit 1; git diff --cached --quiet && exit 0; git commit -m 'tests: checkpoint after successful run'"
          }
        ]
      }
    ]
  }
}
```

**功能：** 只讀一次 JSON stdin，只接受列出的完整普通測試指令，且 `tool_response.exit_code` 明確為零才繼續。版本沒有該欄位時略過提交，不能猜成功。`src/orders.ts`、`tests/orders.test.ts` 換任務確切 owned 檔案；已有 staged 變更就停，避免納入無關工作。add 失敗停止、空 diff 不提交，正常 commit hooks 保留。啟用前離線測成功、失敗及缺少結果輸入。

反模式是在每次 Edit 後加入 `git commit --no-verify`。保持提交檢查；若檢查遞迴觸發自動化，應限縮觸發或重設檢查，不預設略過驗證。啟用前審查 staged diff 與任務所有權。

### Hook 7. 在工作階段開始時注入最新脈絡（SessionStart 搭配 additionalContext）

```json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo 'no-branch'); STATUS=$(git status --short 2>/dev/null | head -10); echo \"{\\\"additionalContext\\\":\\\"Current branch: $BRANCH. Uncommitted changes:\\n$STATUS\\\"}\""
          }
        ]
      }
    ]
  }
}
```

**功能：** 在工作階段開始時，它會收集目前的 Git 分支和未提交變更清單，然後回傳含有 `additionalContext` 欄位的 JSON。Claude Code 會把這段文字注入第一則系統訊息，因此 Claude 從一開始就知道目前所在位置，以及上個工作階段留下了什麼。

<figure>
<blockquote>
<p>請使用 additionalContext 提供 Claude 應該知道的環境目前狀態，或剛完成操作的資訊：環境狀態（目前分支、部署目標、啟用中的功能旗標）、條件式專案規則（剛編輯的檔案適用哪個測試命令）、外部資料（指派給你的未結問題、近期 CI 結果）。</p>
</blockquote>
<figcaption>— Anthropic, hooks reference, https://code.claude.com/docs/en/hooks</figcaption>
</figure>

這個 hook 還能擴充為取得最新 commit 和說明、GitHub Actions 狀態、Linear/Jira 中目前的 sprint，以及目前儲存庫的未結問題。

## 每兩個人就有一個人會遇到的 3 個 hook 陷阱

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

重點摘要

第一個陷阱：exit 1 的 hook 會無聲地放行，Claude 也會忽略錯誤。第二個陷阱：hook 中的繁重工作會在 60 秒逾時後被終止。第三個陷阱：hook 只能看到 `~/.zshrc` 中的環境變數，看不到專案的 `.env`。每個問題都能用一行解決，但如果不知道這些陷阱，就可能以為「hooks 不會動」。

</div>

### 陷阱 1. exit 1 的 hook 會無聲地放行

這是最危險的一個。有一週我以為 `.env` 保護 hook 正常運作，後來才發現我在 hook 中使用了 `jq -r '.tool_input.file_path'`，卻沒有檢查 null。當 `tool_input` 為空時，jq 以代碼 1 結束。Claude 把 exit 1 當成「hook 發生錯誤，繼續執行」，因此動作照常進行。我不明白為什麼 `.env` 還是會被碰到。直到我在終端機用空輸入手動執行 hook，才找到原因。

**修正方法只有一條規則：** 每個 hook 一開始都要檢查 null，並以 `|| exit 0` 結尾。這樣任何意外的結束都會以「安全」方式退出，不會無聲放行。

```bash
# Safe start and end template:
FILE=$(jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
# ... your logic ...
exit 0   # or || exit 0 on one line
```

### 陷阱 2. 執行緩慢的 hooks 會因逾時而被終止

我曾設定一個 hook，在 Edit 後對整個檔案執行 `eslint --fix`。大型檔案（1000+ 行）需要 8–10 秒處理。我知道 Claude Code 的預設 hook 逾時時間是 60 秒，所以以為自己沒有超出限制。但如果一分鐘內發生多次 Edit，hooks 會累積並排隊，有些會直接被略過。直到日誌顯示「hook timed out」，我才發現這點。

**修正方法：** 如果 hook 需要執行繁重工作，就在背景執行。使用 `eslint --fix "$FILE" &`，不要等待結果。這樣 hook 就會在幾毫秒內回傳 exit 0，而實際工作則在背景執行。

```bash
# Heavy operation in background:
npx eslint --fix "$FILE" &
exit 0
```

### 陷阱 3. Hook 看不到專案環境變數

我以為 hook 中的 `process.env` 會像 Node.js 應用程式一樣可用，但事實並非如此。Hook 會在 Claude Code 父程序啟動的 shell 中執行，只能看到 `~/.zshrc` 或 `~/.bashrc` 中的內容，以及 Claude Code 明確傳入的變數。如果你的 hook 依賴 `.env` 中的 `OPENAI_API_KEY`，就必須手動讀取 `.env`。

**修正方法：** 在 hook 命令中明確讀取 `.env`。

```bash
# Passing project env into hook:
cd "$PWD" && source .env && curl -H "Authorization: Bearer $OPENAI_API_KEY" ...
# Or with dotenv:
dotenv -e .env -- node my-validator.js
```

在每個套件都有自己 `.env` 檔案的 monorepo 中，這個問題尤其麻煩。可透過傳入 `PWD`，並在命令開頭明確執行 `cd "$PWD"` 來解決。

## 提交前如何測試 hooks？

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

重點摘要

Hook 就是 shell 命令。你可以在 Claude Code 執行前，先在終端機用假的 stdin 手動執行、檢查結束代碼和 stderr。這能省下好幾個小時的除錯時間，也能避免 hook 封鎖自己的操作。

</div>

**基本測試情境。** 從 JSON 中取出 hook 命令，開啟終端機，並輸入 Claude Code 會傳送的 JSON：

```bash
# Test hook 1 (protect .env):
echo '{"tool_input":{"file_path":".env"}}' | bash -c '
FILE=$(jq -r ".tool_input.file_path // empty")
[ -z "$FILE" ] && exit 0
echo "$FILE" | grep -qE "(^|/)\.env(\.|$)" && {
  echo "BLOCKED" >&2
  exit 2
} || exit 0
'
echo "Exit code: $?"
```

你應該會在 stderr 看到 `BLOCKED` 和 `Exit code: 2`。如果得到 exit 0，代表你的 regex 沒有作用；請修正 grep。

**需要檢查的情境：**

1.  **正向情境** - 檔案應該被封鎖。Hook 1 使用 `.env`，hook 3 使用 `git push --force main`。
2.  **反向情境** - 檔案不應該被封鎖。Hook 1 使用 `README.md`，hook 3 使用 `git push origin feature-branch`。
3.  **邊界情況** - 空輸入、null 值、非預期字元。Hook 應該回傳 exit 0，或以明確錯誤失敗，而不是無聲放行。

**實驗性 hooks 放置位置。** 不要放在 `.claude/settings.json`，而是放在 `.claude/settings.local.json`（不納入 git）。在 `PostToolUse` 下新增 hooks，以 `echo` 取代實際動作；確認它們會觸發後，再移到正式的 `settings.json`。

```json
// Experiment in settings.local.json - writes to /tmp/hook-debug.log:
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"[hook fired] $(date) $(jq -r '.tool_input.file_path // \"?\"')\" >> /tmp/hook-debug.log"
          }
        ]
      }
    ]
  }
}
```

在 Claude Code 中編輯 5–10 次後，開啟 `/tmp/hook-debug.log`，查看哪些檔案觸發了 hook。如果檔案是空的，代表 matcher 不符合；請修正 regex。

## 接下來呢？

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

重點摘要

先從回傳 exit 0 的 PostToolUse hooks 開始；這是最安全的做法，也不會封鎖任何操作。一週後，再加入第一個以 exit 2 結束的 PreToolUse hook，保護 `.env` 或禁止 force-push。兩週後，`CLAUDE.md` 會從 200 行的契約縮減成 50–80 行的簡短脈絡說明，將部分事件檢查移到 hooks，硬存取限制另由權限、sandbox 及遠端保護強制執行。

</div>

接下來一週的 5 個步驟：

1.  **用 Grep 搜尋你的 `CLAUDE.md`**，找出「never」、「forbidden」、「always」和「must」等字詞。每條這類規則都是 hook 的候選項目。如果規則帶有命令語氣，文字檔不會強制執行它。
2.  **從 PostToolUse + exit 0 開始。** 這是最安全的做法：會修改內容，但不會封鎖任何操作。Prettier（hook 2）、終止開發伺服器（hook 5）、測試後自動提交（hook 6）。
3.  **等你有把握時，再加入第一個 PreToolUse + exit 2。** 例如保護 `.env`（hook 1）或禁止 force-push（hook 3）。這是硬性封鎖，如果 grep 命令寫錯，你自己也可能被封鎖。
4.  **提交前手動測試 hooks**（請參閱上方章節）。這能省下好幾個小時的除錯時間。
5.  **用 `settings.local.json` 進行實驗。** 在這裡新增 hooks，以 echo 取代實際動作。

設定好 hooks 後，這個主題群集的下一步是[如何在 Claude Code 中復原變更](https://ailoot.ai/blog/zh-hant-tw/claude-code-undo-changes/)，以防 hook 放過了危險操作。如果你想讓 Claude 在不同工作階段之間記住專案結構，可以在 Claude Code 中建立[第二大腦](https://ailoot.ai/blog/zh-hant-tw/claude-code-second-brain/)（`business/` 資料夾＋連結的知識庫）。若要替專案的 skills 設定名稱以便重複使用，請參閱[Claude Code Skills](https://ailoot.ai/blog/zh-hant-tw/claude-code-skills-library/)。

<div class="section" role="doc-bibliography" aria-label="Sources">

### 來源

- [Claude Code Hooks reference - Anthropic 官方文件](https://code.claude.com/docs/en/hooks)
- [Claude Code Memory documentation - 關於 CLAUDE.md 與系統提示](https://code.claude.com/docs/en/memory)
- [Automate workflows with hooks - Anthropic 官方指南與範例](https://code.claude.com/docs/en/hooks-guide)

</div>





如果你需要透過 Ailoot 使用神經網路，請看看 [Ailoot](https://ailoot.ai/)。

## 常見問題：Claude Code Hooks 2026：逐步設定與 7 個可直接使用的範例

### Hook 為什麼只能讀一次 JSON stdin？

Stdin 是串流，首個 jq 會消耗，後面讀到 EOF。先保存一次輸入，再解析指令與結果；缺少或未知結果視為未驗證，不自動提交。

### 範例 regex hooks 能建立安全邊界嗎？

不能。它們檢查特定事件及可辨識字串，shell 重導向、別名、腳本、MCP 或其他寫入路徑可能繞過。硬限制需受限憑證、檔案權、sandbox 與遠端保護分支。


## Ailoot · 公司及其專案組合

Ailoot以累積的營運經驗、廣大的客戶群、穩定的服務及持續的KPI管理為基礎。

公司及其專案組合：數百萬客戶，每月銷售額達數十億。

Ailoot將這些營運經驗用於API存取，讓客戶的產品、代理、應用程式及自動化流程使用AI功能。
