AI 程式設計教程中文版
從原理到實戰

10 · 怎麼讓操作 100% 執行

Hooks 不是提醒 Claude Code 記得做事,而是在工具呼叫、提示提交、停止前後設定確定性檢查點。能容忍遺漏寫 CLAUDE.md,不能容忍就用 Hook。

翔宇有一條工程習慣:凡是“忘一次就會出事”的規則,不要只寫進提示詞裡。提示詞讓 AI 儘量記住,Hooks(鉤子)讓系統在關鍵節點強制檢查。前者是提醒,後者是門禁。——翔宇

這一篇用 14 分鐘換什麼:第 9 篇講 MCP 讓 Claude Code 夠到外部系統。夠得到以後,風險也變大了。這一篇講 Hooks:怎麼在 Claude Code 準備呼叫工具、呼叫之後、提交提示、準備停止等時間點插入自動檢查,讓“必須發生的事”不再只靠 Claude 記得。

1. 先從一個危險的小動作開始

你在 CLAUDE.md 裡寫了一條規則:

永远不要修改 `.env` 文件。

這條規則有用嗎?有用。Claude Code 會讀到,也大機率遵守。

但“大機率”不是“保證”。

想象某一次任務裡,上下文很長、檔案很多、你又讓它“直接修好本地配置”。Claude 準備寫 .env。如果只靠 CLAUDE.md,它可能會想起規則,也可能在複雜上下文裡漏掉。

.env 不是普通檔案。改壞一次,可能就是資料庫連線、生產 token、第三方金鑰出問題。

這時你不應該再問“怎麼讓提示詞寫得更嚴厲”。你應該換一個機制。

第一性原理:Hooks 解決的不是“Claude 不理解規則”,而是“某些規則不能靠模型記憶來執行”。

Claude Code 官方 Hooks guide 對 Hooks 的定位很清楚:Hooks 會在 Claude Code 生命週期的特定點自動執行,用來 enforce project rules、automate repetitive tasks、integrate with existing tools。也就是說,它是確定性自動化,不是又一段勸 Claude 聽話的提示詞。

2. CLAUDE.md、Permissions、Hooks 各管什麼

先把三個概念擺清楚。

機制中文理解強度適合什麼
CLAUDE.md工作說明書建議 / 習慣風格、流程、偏好、專案背景
Permissions許可權閘門允許 / 詢問 / 拒絕工具級訪問控制
Hooks自動檢查點到點必觸發格式化、審計、阻止危險動作、補上下文

舉一個 TypeScript 專案:

  • “優先用現有 service 層,不要新造抽象”適合寫 CLAUDE.md
  • “Bash 裡執行 rm -rf 要詢問”適合許可權規則。
  • “每次改 .ts 檔案後跑 formatter”適合 PostToolUse Hook。
  • “任何寫 .env 的嘗試都攔下來”適合 PreToolUse Hook。

這三者不是互相替代。

CLAUDE.md 給方向,Permissions 管門檻,Hooks 做檢查點。

一句話理解:能容忍偶爾遺漏的,寫進 CLAUDE.md;需要工具級邊界的,用 Permissions;到某個事件點必須自動發生的,用 Hooks。

3. Hook 其實是生命週期檢查點

Hook 的本質是:Claude Code 執行到某個時間點,自動把一段 JSON 輸入交給你配置的 handler(處理器)。handler 可以是命令、HTTP 端點、MCP 工具、prompt 或 agent。

先看一個簡化時間線:

flowchart LR
    Prompt["使用者提交提示"]
    Think["Claude 思考"]
    Pre["PreToolUse<br/>工具執行前"]
    Tool["工具呼叫<br/>Edit / Write / Bash / MCP"]
    Post["PostToolUse<br/>工具成功後"]
    Stop["Stop<br/>準備停止"]

    Prompt --> Think --> Pre --> Tool --> Post --> Think --> Stop

    style Pre fill:#fef3c7,stroke:#f59e0b,stroke-width:2px
    style Post fill:#dcfce7,stroke:#22c55e,stroke-width:2px
    style Stop fill:#f3e8ff,stroke:#a855f7,stroke-width:2px

Hooks 不是隻在“寫檔案前”能用。官方 Hooks reference 列了很多事件,比如:

  • UserPromptSubmit:使用者提示提交時觸發,適合注入上下文、攔截危險請求。
  • PreToolUse:工具執行前觸發,適合阻止危險命令、改寫工具輸入。
  • PermissionRequest:即將彈許可權請求時觸發,適合自動允許/拒絕某類請求。
  • PostToolUse:工具成功後觸發,適合格式化、審計、補充反饋。
  • PostToolUseFailure:工具失敗後觸發,適合記錄失敗、給 Claude 修復線索。
  • Stop:Claude 準備停下時觸發,適合檢查任務是否真的完成。
  • SubagentStop:子代理準備結束時觸發,適合審查子代理結果。
  • PreCompact / PostCompact:上下文壓縮前後觸發,適合歸檔或重新注入關鍵上下文。
  • Elicitation / ElicitationResult:MCP server 請求使用者輸入前後觸發,適合審計或自動處理資訊徵詢。

新手不需要一口氣記完。先記三個最常用的:

  • PreToolUse:動手前攔。
  • PostToolUse:動手後補。
  • Stop:收工前查。

Hook 觸發順序到底是什麼樣:一次完整工具呼叫的事件鏈是 UserPromptSubmit → (思考) → PreToolUse → (Permission 檢查) → 工具執行 → PostToolUse / PostToolUseFailure → (思考) → Stop關鍵事實:① PreToolUse 在 Permission 檢查之前觸發——Hook 可以提前攔掉危險操作不彈許可權窗;② Stop 在準備結束本輪之前觸發——可以阻止 Claude 過早收工。所以 Hook 不是"事後審計",是主動嵌入決策鏈路——這是它跟"日誌"的本質區別。

新手坑:不要為了“控制感”給每個事件都掛 Hook。Hook 越多,執行越慢,干擾越多,排查也越難。

4. 一個最小 Hook 長什麼樣

Hooks 寫在 settings JSON 裡。結構有三層:

  1. 選事件,比如 PreToolUse
  2. 選 matcher group,比如只匹配 WriteBash
  3. 放一個或多個 hook handler。

比如阻止寫 .env

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/protect-env.sh"
          }
        ]
      }
    ]
  }
}

指令碼大概這樣:

#!/usr/bin/env bash
set -euo pipefail

INPUT="$(cat)"
FILE_PATH="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')"

case "$FILE_PATH" in
  *.env|*/.env|*.env.*)
    echo "Blocked: environment files may contain secrets and must be edited manually." >&2
    exit 2
    ;;
esac

exit 0

Claude Code 會把事件 JSON 透過 stdin 傳給 command hook。上面指令碼讀 tool_input.file_path,如果目標是環境檔案,就返回 exit code 2。

關鍵點:Hook 不需要說服 Claude。它在工具執行前已經拿到了結構化輸入,可以直接基於檔案路徑、命令、許可權模式做判斷。

5. exit code 2 不只是“報錯”

官方 Hooks reference 明確說:exit 0 表示成功;exit 2 表示 blocking error;其他退出碼對多數事件來說是 non-blocking error。

這很重要:

  • exit 0:成功,動作繼續;stdout 裡的 JSON 可能被解析。
  • exit 2:阻止;stderr 會反饋給 Claude。
  • exit 1 或其他:多數事件下只是非阻塞錯誤,動作繼續。

所以不要用傳統直覺寫:

exit 1

如果你想阻止操作,要寫:

echo "Blocked: do not edit .env" >&2
exit 2

並且記住一個細節:exit 2 時,Claude Code 忽略 stdout 和 stdout 裡的 JSON,只看 stderr 作為反饋。 如果你要用 JSON 做更細控制,通常是 exit 0 然後在 stdout 輸出 JSON。

例如 PreToolUse 可以返回:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Database writes are not allowed."
  }
}

實操判斷:簡單阻止用 stderr + exit 2;需要更細的 allow / deny / ask / 修改輸入,用 exit 0 + JSON output

6. 五種 handler:不只有 shell

舊理解裡,Hook 常被理解成“自動跑一個 shell 指令碼”。現在已經不止這樣。

官方 Hooks reference 裡的 handler type 有五類:

  • command:執行 shell 命令,適合格式化、檔案保護、日誌。
  • http:POST 到一個 URL,適合通知、審計、接外部系統。
  • mcp_tool:調已連線 MCP server 的工具,適合複用現有 MCP 能力。
  • prompt:讓 Claude 模型做一次判斷,適合檢查是否真的完成、是否符合複雜規則。
  • agent:啟動帶工具的 verifier agent,適合需要讀檔案、搜尋後再判斷的場景。

這五種放在一條光譜上看:

确定性强 -> 判断力强
command -> http -> mcp_tool -> prompt -> agent

例子:

  • .ts 後跑 Prettier:command
  • 每次許可權請求發審計系統:http
  • 複用一個內部 policy MCP server:mcp_tool
  • Stop 前判斷“任務是否真的完成”:prompt
  • Stop 前讓一個 verifier agent 搜尋測試結果、讀 diff 再判斷:agent

不要反過來用重武器:能用 command 判斷的,就不要用 prompt;能用 prompt 判斷的,不一定要開 agent。判斷越智慧,成本和不確定性也越高。

為什麼 Anthropic 設計 5 種 handler 而不是隻給 command:用 shell command 解決一切是最簡單——但有些判斷 shell 寫不出來。"任務真的完成了嗎"需要語義理解,正則做不到——這就是 prompt handler 的位置。"任務完成了但程式碼風格還有 3 處問題"需要讀程式碼 + 跑命令——這就是 agent handler。5 種遞進的設計哲學是:讓你按"判斷複雜度"選合適的工具,不強迫所有規則都在 shell 裡寫——避免出現"寫 100 行 shell 模擬模型推理"的反模式。

7. Block-at-Write 還是 Block-at-Submit

Hook 很容易被用重。

比如你想保證測試透過。你可以在每次 Edit 後立刻跑全量測試嗎?技術上可以。但這會讓 Claude 每改一個檔案就等很久,任務體驗會崩。

更好的方式是區分兩類規則:

  • Block-at-Write:動手前或剛動手後立即攔,適合改 .env、刪庫、寫生產配置。
  • Block-at-Submit:收工前統一檢查,適合測試、lint、文件完整性、驗收清單。

危險動作要早攔,因為一旦發生就可能造成損失。

質量檢查可以晚一點,因為 Claude 需要先完成一組相關修改,再統一修 lint、測試和格式。

Stop hook 就適合 Block-at-Submit:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "复盘本轮结果。如果还缺测试或必要的验证步骤,就阻止停止:$ARGUMENTS"
          }
        ]
      }
    ]
  }
}

底層邏輯:早攔保護不可逆風險,晚查保護交付質量。不要把所有質量規則都塞到每一次檔案寫入前。

8. Hook 配置放在哪裡

Hook 遵守 Claude Code settings scope:

  • ~/.claude/settings.json:作用於你所有專案,適合個人通知、個人日誌。
  • .claude/settings.json:作用於當前儲存庫所有協作者,適合團隊規範、安全規則。
  • .claude/settings.local.json:只作用於當前儲存庫你自己,適合本機實驗、臨時除錯。
  • plugin:作用於外掛啟用處,適合外掛隨帶的 Hook。
  • managed settings:作用於組織級,適合安全和合規強制策略。

官方配置文件裡,scope 優先順序大致是:managed 最高,其次命令列引數、local、project、user。專案級能覆蓋使用者級,managed 不能被普通使用者覆蓋。

你可以用:

/hooks

開啟只讀 hooks 選單,檢查當前有哪些 hook、來自哪個來源、匹配什麼事件。官方說明裡 /hooks 是 read-only:想改配置,還是要編輯 settings JSON 或讓 Claude 修改檔案。

團隊規則別放錯:團隊必須共同遵守的 Hook 放 .claude/settings.json;只對你本機有意義的通知、路徑和實驗,放 .claude/settings.local.json 或使用者級。

9. 冪等性和效能

Hooks 是程式碼。程式碼就有工程質量。

第一條是冪等性。

冪等性就是:跑一次和跑多次,結果不應該越來越壞。

好的 Hook:

格式化文件
检查文件路径
追加带唯一 id 的审计日志

危險 Hook:

每触发一次就插一条数据库记录
每触发一次就覆盖一个全局配置
每触发一次就发一条无法去重的外部通知

Claude Code 可能多次編輯同一個檔案,PostToolUse 就可能多次觸發。如果 Hook 沒有冪等性,就會製造重複副作用。

第二條是效能。

PreToolUse 是熱路徑。你在每次工具呼叫前跑一個 10 秒指令碼,Claude Code 體驗會立刻變慢。

減少成本的方法:

  • matcher 限定工具名。
  • if 進一步限定命令或檔案型別。
  • 小檢查用 command,不要動不動 prompt/agent。
  • 長任務考慮 async,但不要用 async 做必須同步攔截的安全規則。

Hook 設計原則:越靠前、越高頻的事件,Hook 越要短、準、可預測。

10. 這一篇和前幾篇怎麼連起來

到這裡,Claude Code 的控制層可以串成一張圖。

flowchart TB
    Need["你想控制 Claude Code 行為"]
    Know["長期知識和偏好<br/>CLAUDE.md"]
    Reuse["重複任務流程<br/>Skills"]
    Reach["連線外部系統<br/>MCP"]
    People["隔離或協作<br/>SubAgents / Agent Teams"]
    Guard["確定性檢查點<br/>Hooks"]
    Perm["工具訪問邊界<br/>Permissions"]

    Need --> Know
    Need --> Reuse
    Need --> Reach
    Need --> People
    Need --> Guard
    Need --> Perm

    style Guard fill:#fef3c7,stroke:#f59e0b,stroke-width:2px
    style Perm fill:#fee2e2,stroke:#ef4444,stroke-width:2px

每一層解決的問題不同:

  • CLAUDE.md:讓 Claude 知道專案規則,關鍵詞是 should。
  • Skills:複用任務流程,關鍵詞是 reusable workflow。
  • SubAgents:隔離旁支任務,關鍵詞是 separate context。
  • Agent Teams:多會話協作,關鍵詞是 shared task list。
  • MCP:夠到外部系統,關鍵詞是 external tools。
  • Hooks:到點自動檢查,關鍵詞是 deterministic checkpoint。
  • Permissions:哪些工具能用,關鍵詞是 access control。

第 11 篇會專門講 Permissions。先記住邊界:Hook 可以在事件點做檢查,Permissions 決定工具訪問規則。兩者經常一起用,但不是一回事。

一句話理解:Hooks 是“到點必查”的檢查點;Permissions 是“能不能用工具”的訪問邊界;CLAUDE.md 是“希望你怎麼做”的說明書。

11. 本章自檢

試著用自己的話回答:

  1. 為什麼"我已經寫進 CLAUDE.md"不能等同於"這件事一定會發生"?對應 §1-§2。
  2. exit 2 和 exit 1 在 Hooks 裡有什麼關鍵區別?如果你想阻止寫 .env,應該用哪個?對應 §5。
  3. 動手題 ⭐:在你專案根新建 .claude/hooks/protect-secrets.sh,寫一個 PreToolUse Hook:當 Claude 想 Edit / Write 任何檔名包含 .env / secret / credentials 的檔案時,用 stderr 輸出"危險檔案不允許編輯"+ exit 2 阻止。然後在 .claude/settings.json 註冊。自檢:手動讓 Claude 試著改 .env,看 Hook 有沒有攔下來 + 看 stderr 資訊有沒有進 Claude 上下文。這一道題做完你會知道:CLAUDE.md 提醒 ≠ 真攔得住,Hook 才是確定性。對應 §1 + §4。

過關標準:能用一句話說清:Hooks 是 Claude Code 生命週期裡的確定性檢查點,用來自動執行、阻止或反饋那些不能只靠模型記憶完成的規則。

本篇術語速查表
  • Hook:鉤子,在 Claude Code 生命週期特定點自動執行的處理器。
  • handler:處理器,Hook 觸發後真正執行的 command、http、prompt 等。
  • matcher:匹配器,限定 Hook 對哪些工具或事件生效。
  • PreToolUse:工具執行前,工具呼叫前觸發,可阻止危險動作。
  • PostToolUse:工具執行後,工具成功後觸發,常用於格式化和審計。
  • Stop:停止前,Claude 準備結束當前回合時觸發。
  • exit code:退出碼,指令碼結束時返回給 Claude Code 的狀態數字。
  • stderr:標準錯誤輸出,exit 2 時反饋給 Claude 的錯誤資訊來源。
  • idempotency:冪等性,多次執行結果仍然安全穩定。
  • Permissions:許可權系統,控制 Claude Code 工具訪問邊界的機制。

官方資料

接下來去哪

如果只記一個判斷:凡是“漏一次就會出事”的規則,不要只寫成提醒,要放到 Hook 或 Permission 裡。

本頁目錄