跳到主要內容

【2026 最新】Claude Code Hooks 教學:新手實作 3 道閘門與故障驗收

最後更新: ·
Claude Code Hooks 三道閘門:事件觸發、權限攔截與自動驗收

你明明在 CLAUDE.md 寫了「改完檔案要跑 formatter、不能碰 .env、危險命令先問」,Claude Code 卻可能在長任務裡漏掉其中一步。這篇 Claude Code Hooks 教學不會把 Hook 說成萬能安全鎖;我們會把規則拆成事件、條件與程式,親手做出格式化、測試收據與危險操作攔截三道閘門。

你不需要先懂 Agent 架構。讀完會知道 CLAUDE.md、Skill、Subagent、Hook 各自該放什麼,能把兩支 Python 腳本與一份 settings.json 放進練習專案,並用 no-decision/ask/deny、timeout、故障注入與 With/Without A/B 設計驗收。本文程式只在 macOS fixture 做離線驗證;Linux 與 Windows 尚未實測。Windows 不能只換 Python 路徑:PowerShell 需要自己的命令解析與測試,npm 的 .cmd.bat shim 也需要不同啟動方式。

先記住本文的實作式:command Hook =事件(何時)+條件(哪些呼叫)+Handler(執行什麼)。CLAUDE.md 是把規則交給模型判斷;command Hook 則是由 Claude Code harness 在符合事件時啟動程式。Claude Code 目前也支援 HTTP、MCP tool、prompt 與 agent Hook;它們的成本與失敗契約不同,不能全部等同於本機程式。啟動比較確定,不代表程式一定成功,也不代表所有路徑都被覆蓋。

先說結論:Claude Code Hooks 教學先抓住 5 件事

  • 每個 Session 都要知道的背景放根目錄 CLAUDE.md;可重複的操作流程放 Skill;需要隔離脈絡或平行研究交給 Subagent;必須在指定生命週期觸發的動作才用 Hook。
  • PreToolUse 在工具執行前:本文命中規則時回 askdeny;未命中時 exit 0、stdout 留空,讓呼叫回到正常 permissions 流程。defer 只在非互動 claude -p、單一 tool call 的暫停/resume 整合生效,不是「沒有決策」的同義詞。
  • PostToolUse 在工具成功後:適合 formatter、lint、test 與收據;它發生時原始編輯已經完成,不能倒轉副作用。
  • 同一事件所有匹配 Hook 目前會平行執行:若 test 必須等 formatter,請放進同一個 wrapper 依序跑;這只保證單次 Handler 內的順序,不能依賴 sibling Hook 的排列。
  • Hook 是 defense in depth:timeout、找不到執行器、async 與事件覆蓋都可能留下缺口;permissions、sandbox、pre-commit 與 CI 仍要各自成立。

Claude Code Hooks 教學的核心:三道閘門怎麼流動?

把 Claude Code 想成廚師,Hook 是裝在廚房動線上的感應器。第一道在食材進鍋前看「這個動作能不能做」;第二道在切完菜後自動整理格式;第三道才拿結果去驗收。感應器會被事件觸發,但如果線路斷了、逾時或裝錯門口,安全仍要由門鎖(permissions/sandbox)兜底。

Claude Code 工具呼叫依序經過 PreToolUse 風險判斷、工具執行、PostToolUse 格式化與測試收據的三道閘門
PreToolUse 決定是否放行,PostToolUse 再做品質驗收;permissions 與 CI 是獨立的外層保護。

官方 Hooks reference列出完整事件與輸入/輸出 schema;本文只實作 PreToolUsePostToolUse。若你習慣把所有規則塞進一個檔案,可先看 CLAUDE.md 指令膨脹整理法,再把真正需要事件觸發的動作搬出來。

CLAUDE.md、Skill、Subagent、Hook 怎麼選?

Anthropic 在 2026 年 6 月公布的官方選擇指南把差異講得很直接:根目錄 CLAUDE.md 會留在 Session 脈絡;Skill 在需要時載入完整流程;Subagent 使用獨立 context,只把結果帶回;Hook 則在生命週期事件上觸發。把「我希望模型記得」和「程式要確實被啟動」分開,選擇就容易很多。

CLAUDE.md、Skill、Subagent 與 Hook 的載入時機、適用工作與限制比較
判斷關鍵不是檔案名稱,而是「誰來執行」與「何時必須發生」。
  • CLAUDE.md專案架構、build 指令、團隊命名規則等,每個 Session 都應知道的背景。
  • 選 Skill:release checklist、code review、資料遷移等,可被叫用並由模型執行的程序。
  • 選 Subagent:深度搜尋、log 分析、dependency audit 等,過程會塞滿主對話、適合隔離或平行處理的工作。
  • 選 Hook:每次寫檔後跑 formatter、工具呼叫前檢查命令、壓縮前備份紀錄等,事件本身就是觸發條件的自動化。

Hook 和「讓模型使用外部工具」也不是同一件事。想理解第三方 Agent Hooks 的掛載思路,可讀 Flue 2 Agent Hooks 實戰;本文處理的是 Claude Code 原生 lifecycle hook 與本機設定。

準備:先建立可回滾的練習專案

不要從公司主倉庫開始。先複製一個沒有 Secret、可隨時刪除的 fixture repo,確認目前的 npm test 與 Prettier 都能單獨成功,再建立以下結構:

GitHub release 頁面在 2026 年 8 月 28 日將 v2.1.248 標為 Latest。先執行 claude --version 把版本寫進收據;如果你的版本較舊,不要假設本文的 exec form、matcher、exit 或 timeout 行為完全相同,升級前後都要重跑 canary。

mkdir -p .claude/hooks .claude/hook-logs
touch .claude/hooks/risk_gate.py
touch .claude/hooks/quality_gate.py

printf '%s\n' '.claude/hook-logs/' >> .gitignore
# 專案尚未固定 formatter 時才安裝;已有工具就沿用原命令
npm install --save-dev --save-exact prettier
./node_modules/.bin/prettier --version
npm test

安裝動作只在人工準備階段做一次;Hook 之後直接呼叫專案的 node_modules/.bin/prettier,缺少相依時就留下失敗收據,不在背景下載套件。先把 npm test 換成你專案真正的固定測試命令。之後建立 .claude/settings.json;這是嚴格 JSON,不能放註解或尾端逗號:

{
  "permissions": {
    "ask": [
      "Bash(rm *)",
      "Bash(git push *)",
      "Bash(git reset --hard *)",
      "Bash(git clean *)"
    ],
    "deny": [
      "Read(.env)",
      "Read(.env.*)",
      "Read(**/*.pem)",
      "Read(**/*.key)",
      "Read(**/id_rsa)",
      "Read(**/id_ed25519)",
      "Read(**/*.p12)",
      "Read(**/*.pfx)",
      "Edit(.env)",
      "Edit(.env.*)",
      "Edit(**/*.pem)",
      "Edit(**/*.key)",
      "Edit(**/id_rsa)",
      "Edit(**/id_ed25519)",
      "Edit(**/*.p12)",
      "Edit(**/*.pfx)"
    ]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/risk_gate.py"],
            "timeout": 5
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "python3",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/quality_gate.py"],
            "timeout": 150
          }
        ]
      }
    ]
  }
}

這裡刻意使用 exec form:command 只放執行器,路徑放進 args 陣列,不讓 shell 重新解讀空格、反引號或 $()。多數事件的 command/HTTP/MCP tool Handler 預設 timeout 是 600 秒;這不是官方列出的全域上限,UserPromptSubmit 預設 30 秒、MessageDisplay 10 秒,SessionEnd 另有共享預算。我們替風險檢查設 5 秒、品質 wrapper 設 150 秒,腳本內再限制子程序。

第 1、2 道閘門:PostToolUse 先格式化,再測試並留收據

痛點是「格式化和測試都要跑,但不能搶跑」。解法是單一同步 wrapper:收到成功的 EditWrite 後,先驗證檔案仍在目前工作目錄,再依序跑 Prettier 與 npm test。這個安全版刻意丟棄子程序 stdout/stderr 與 argv,不保存 tool_input.contenttool_response;收據只留輸入 SHA-256、相對路徑、固定步驟名稱、exit code、耗時與 timeout/spawn_error 類型。

#!/usr/bin/env python3
import datetime as dt, hashlib, json, os, subprocess, sys, time
from pathlib import Path

MAX_INPUT = 1024 * 1024
SUFFIXES = {".css", ".html", ".js", ".json", ".jsx", ".md", ".ts", ".tsx"}
SECRET_NAMES = {".env", ".git", "id_rsa", "id_ed25519"}
SECRET_SUFFIXES = {".key", ".pem", ".p12", ".pfx"}

def run(step, argv, root, timeout):
    started = time.monotonic()
    try:
        result = subprocess.run(
            argv, cwd=root, stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL, timeout=timeout, check=False
        )
        code, error = result.returncode, None
    except subprocess.TimeoutExpired:
        code, error = 124, "timeout"
    except OSError:
        code, error = 127, "spawn_error"
    record = {
        "step": step,
        "exit_code": code,
        "duration_ms": round((time.monotonic() - started) * 1000)
    }
    if error:
        record["error"] = error
    return record

raw = sys.stdin.buffer.read(MAX_INPUT + 1)
if len(raw) > MAX_INPUT:
    raise SystemExit(2)

try:
    event = json.loads(raw)
    cwd = event["cwd"]
    file_path = event["tool_input"]["file_path"]
    if not isinstance(cwd, str) or not isinstance(file_path, str):
        raise TypeError
    root = Path(cwd).resolve()
    path = Path(file_path).resolve()
    relative = path.relative_to(root)
except (json.JSONDecodeError, KeyError, TypeError, ValueError):
    print("invalid or outside-cwd hook input", file=sys.stderr)
    raise SystemExit(2)

if event.get("hook_event_name") != "PostToolUse":
    raise SystemExit(2)
if event.get("tool_name") not in {"Edit", "Write"}:
    raise SystemExit(2)
if (any(part in SECRET_NAMES for part in relative.parts)
        or path.suffix.lower() in SECRET_SUFFIXES):
    print("sensitive path reached quality hook", file=sys.stderr)
    raise SystemExit(2)
if not path.is_file() or path.suffix.lower() not in SUFFIXES:
    raise SystemExit(0)

steps = [
    run("prettier",
        [str(root / "node_modules/.bin/prettier"), "--write", str(path)],
        root, 20)
]
if steps[-1]["exit_code"] == 0:
    steps.append(run("test", ["npm", "test"], root, 120))

receipt = {
    "ts": dt.datetime.now(dt.timezone.utc).isoformat(),
    "event": "PostToolUse",
    "relative_path": str(relative),
    "input_sha256": hashlib.sha256(raw).hexdigest(),
    "steps": steps
}
log_dir = root / ".claude" / "hook-logs"
line = (json.dumps(receipt, ensure_ascii=False) + "\n").encode()
try:
    log_dir.mkdir(parents=True, exist_ok=True)
    fd = os.open(log_dir / f"{dt.date.today().isoformat()}.jsonl",
                 os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
    try:
        os.write(fd, line)
    finally:
        os.close(fd)
except OSError:
    print("quality receipt write failed", file=sys.stderr)
    raise SystemExit(2)

failed = next((step for step in steps if step["exit_code"] != 0), None)
if failed:
    print(f"quality gate failed: {failed['exit_code']}", file=sys.stderr)
    raise SystemExit(2)

這個最小範例假設 Hook input 的 cwd 就是 npm package root,並省略跨程序 file lock 與 monorepo 的套件路由。單一 Session 的平行 tool calls 也可能同時觸發多個 PostToolUse,多 Session 更會放大競爭;只要可能重疊,就要加 lock/queue,或交給 CI 序列化。log 本身若無法建立,Handler 只能回 generic stderr 與 exit 2,不可能留下那張收據。

同步測試會增加每次 edit 的等待時間;formatter → test 的順序只對單次 wrapper 呼叫成立。若 suite 很慢,可以在驗收完成後改成 asyncRewake,但 async Hook 的結果不能阻止原動作,而且測試可能與下一次編輯重疊;它適合提醒與 telemetry,不應是唯一的 merge gate。多工作樹並行時,也可搭配 Proliferate Worktree 流程分開收據。

第 3 道閘門:PreToolUse 做 deny/ask,秘密檔案交給 permissions

痛點是危險命令不只一種寫法,而且 Hook 腳本本身也拿到使用者權限。下面的窄範圍分類器只辨識本文列出的直接、頂層 rmgit 拼法,並把管線、背景執行、command substitution 與 sudo 交給人工確認;根目錄強刪回 deny,常見遠端寫入或資料刪除回 ask,其餘不做決策,讓正常 permissions 流程繼續。

#!/usr/bin/env python3
import json, re, shlex, sys

MAX_INPUT = 256 * 1024

def reply(kind, reason):
    print(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": kind,
            "permissionDecisionReason": reason
        }
    }, ensure_ascii=False))

def segments(command):
    parsed = []
    for part in re.split(r"(?:&&|\|\||;|\n)", command):
        try:
            tokens = shlex.split(part)
        except ValueError:
            return []
        if tokens:
            parsed.append(tokens)
    return parsed

raw = sys.stdin.buffer.read(MAX_INPUT + 1)
if len(raw) > MAX_INPUT:
    raise SystemExit(2)

try:
    event = json.loads(raw)
    command = event["tool_input"]["command"]
    if not isinstance(command, str):
        raise TypeError
except (json.JSONDecodeError, KeyError, TypeError):
    print("invalid hook input", file=sys.stderr)
    raise SystemExit(2)

if event.get("hook_event_name") != "PreToolUse":
    raise SystemExit(2)
if event.get("tool_name") != "Bash":
    print("invalid hook input", file=sys.stderr)
    raise SystemExit(2)

lowered = command.lower()
if re.search(r"(?:curl|wget)\b[^\n]*(?:\||\|&)\s*(?:ba|z|fi)?sh\b", lowered):
    reply("ask", "先下載與檢查,再分開交給 shell 執行。")
    raise SystemExit(0)
if re.search(r"(?<!\|)\|(?!\|)|(?<!&)&(?!&)|\$\(|`", command):
    reply("ask", "命令含複合 shell 語法,需要人工確認。")
    raise SystemExit(0)

parts = segments(command)
if not parts and command.strip():
    reply("ask", "命令無法解析,需要人工確認。")
    raise SystemExit(0)

for tokens in parts:
    exe, args = tokens[0].rsplit("/", 1)[-1].lower(), tokens[1:]
    if exe in {"sudo", "env", "xargs"}:
        reply("ask", "命令使用權限或間接執行 wrapper,需要人工確認。")
        raise SystemExit(0)
    if exe == "rm":
        flags = "".join(a[1:] for a in args if a.startswith("-")).lower()
        targets = [a.rstrip("/") or "/" for a in args if not a.startswith("-")]
        if "r" in flags and "f" in flags and any(
            t in {"/", "~", "$HOME", "${HOME}"}
            or t.startswith(("/*", "~/", "$HOME/", "${HOME}/"))
            for t in targets
        ):
            reply("deny", "拒絕遞迴強制刪除根目錄或家目錄。")
            raise SystemExit(0)
        if "r" in flags or "f" in flags:
            reply("ask", "rm 含遞迴或強制旗標。")
            raise SystemExit(0)
    if exe == "git" and args:
        if "push" in args:
            reply("ask", "git push 會改變遠端狀態。")
            raise SystemExit(0)
        if args[:2] == ["reset", "--hard"]:
            reply("ask", "git reset --hard 可能丟失未提交變更。")
            raise SystemExit(0)
        if args[0] == "clean" and {"f", "d"}.issubset(set(
            "".join(a[1:] for a in args[1:] if a.startswith("-"))
        )):
            reply("ask", "git clean -fd 會刪除未追蹤內容。")
            raise SystemExit(0)

這不是安全邊界或完整 shell parser:bash -c、Python 動態組命令、alias 或自訂 wrapper 都可能不命中,回到正常 permissions。Claude Code v2.1.248 本身已有 critical-path permission circuit breaker;依 permission mode 會進入詢問、classifier 或拒絕。本文用 rm -rf / 當容易觀察的 canary,不代表只有這支 Hook 才能處理它。

秘密檔案更不能只靠 PreToolUse@file 參照不會觸發 PreToolUse,所以範例加入 ReadEdit deny 規則;這類規則對內建工具與 Claude Code 能辨識的檔案命令是 best effort。.env.* 也會封鎖 .env.example,需要公開模板時請縮窄成實際秘密檔名。任意 Python/Node 子程序仍可能直接讀檔;Claude Code sandbox 可限制 Bash tool 與其子程序,但 command Hook 本身仍以使用者權限執行,不能把 sandbox 當成 Hook 的完整隔離層。可搭配 Claude Code Auto Mode 安全實驗一起驗收。

怎麼測?先做 fixture,再做 With/Without A/B

本文寫作 shell 的 PATH 找不到 Claude Code CLI,因此沒有做 live Hook integration test,也沒有捏造遵循率或 token 節省數字。現有離線證據只驗證:把合成 JSON 送進 Handler 時,npm test 路徑不輸出 decision、rm -rf / 回 deny、git push 回 ask,PostToolUse wrapper 對 cwd 外 fixture input 回 exit 2;另有本機 npm fixture 收據記錄 Prettier 後接測試。它們不驗證 settings 載入、matcher、workspace trust、Claude Code 的 exit/timeout 整合、平行執行、token 或延遲,也不代表 PostToolUse 能撤銷原編輯。

# 未決策:stdout 應為空、exit 0,之後仍走正常 permissions
printf '%s\n' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npm test"}}' |
  python3 .claude/hooks/risk_gate.py

# 阻擋:JSON 應含 permissionDecision="deny"
printf '%s\n' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' |
  python3 .claude/hooks/risk_gate.py

# 詢問:JSON 應含 permissionDecision="ask"
printf '%s\n' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"git push origin main"}}' |
  python3 .claude/hooks/risk_gate.py

# 複合語法:應 ask,不執行字串中的命令
printf '%s\n' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"echo ok | rm -rf /"}}' |
  python3 .claude/hooks/risk_gate.py
printf '%s\n' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"sudo rm -rf /"}}' |
  python3 .claude/hooks/risk_gate.py
printf '%s\n' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"git -C /tmp/repo push origin main"}}' |
  python3 .claude/hooks/risk_gate.py

下面兩行只用來人工切換 Control/Treatment,不是可重跑的 A/B harness。正式 paired A/B 要讓兩組使用獨立但相同的 repo snapshot,固定模型、effort、prompt、permissions、max turns/budget 與 session persistence,隨機化每對的執行順序,並保存 stream JSON、Hook lifecycle、token、wall time 與 decision。官方提供 disableAllHooks;但 managed hooks 只能由 managed settings 停用,因此企業環境要先確認控制組真的沒有同一個 Handler。

# .claude/control-settings.json
{"disableAllHooks": true}

# Control:手動新開 Session,載入停用設定
claude --settings .claude/control-settings.json

# Treatment:另開新 Session,使用專案 Hooks
claude
Claude Code Hooks With 與 Without 對照實驗的配對條件、收據指標與通過門檻
先用 10 組 paired smoke test 找明顯故障;若要聲稱成功率差異,再擴大樣本並報分母與不確定性。
  1. 觸發覆蓋率:應觸發事件數和實際收據數是否一致;少一張就先查 matcher、路徑、執行器與 trust。
  2. 決策正確率:預先標註 no-decision/ask/deny,另行記錄工具最後是 allowed/prompted/denied,計算 false allow 與 false block。
  3. 品質結果:formatter exit、test exit、Claude 修復後是否回到綠燈;PostToolUse 失敗不等於原編輯已回滾。
  4. 延遲:看每個 Hook 與整體任務的 p50/p95,而非只拿一次最快結果。
  5. 脈絡成本:command Hook 的設定不必整段塞進主 context,但錯誤輸出可能回到對話;以整體 Session 用量比較,不能宣稱「Hook 零 token」。

做完第一輪,可用 Claude Code 規格修正殘留流程檢查控制組與處理組是否混入前一輪指令;若要把 A/B 變成可重跑 eval,則把 prompt、snapshot、預期 decision 與收據格式固定下來。

本文優先驗證的 5 個失敗模式

1. 把 exit 1 當成任何事件都會阻擋

command Hook 的 exit code 是事件相關契約。本文的 PreToolUse 命中時使用結構化 permissionDecision;輸入、路徑或收據驗證失敗才 exit 2。對多數事件,exit 1 且 stdout 為空或純文字只是非阻擋錯誤;有效 structured JSON 仍可帶決策,WorktreeCreate 也有不同契約。尤其 PostToolUse 已在副作用之後,不能把「exit 2 會擋」外推成回滾。

2. 以為 PreToolUse timeout 會自動 fail closed

截至 2026 年 8 月 28 日,官方 reference 對 command/HTTP/MCP 的 PreToolUse timeout 行為,是回到正常 permissions 流程,不是自動 deny。復原方式是縮短 Handler、監看缺收據,並用 permissions/managed settings 保存真正的硬規則。

3. 讓多個匹配 Hook 共享未定義順序

官方目前會平行執行所有匹配 Hook;單一 Session 的平行 tool calls 也能同時啟動多個 wrapper。一個改檔、另一個立刻測試,就可能產生 race。單次呼叫內需要順序的步驟放在同一 wrapper;跨呼叫仍要 lock/queue/CI,互不相依的通知與 metrics 才拆開。

4. 把完整 stdin/stdout 傳到 HTTP 或 log

Hook input 可能帶有 tool input、路徑、transcript path 等資料;HTTP Hook 還會把事件 JSON 發到 endpoint。本範例直接丟棄子程序 stdout/stderr,只記 allowlist 過的欄位;不要以 regex 遮罩冒充完整 secret scanner。若 Handler 來源不明,先當成程式碼供應鏈審查,不要直接啟用。

5. 在不可信 repo 直接啟動自動化

Hook command 以使用者權限執行。互動 Session 會暫停 settings-file Hooks,直到你信任該資料夾或可延伸信任的 parent;-p/SDK 不顯示對話框,而是把資料夾視為 trusted。處理陌生倉庫前先審查 .claude/。可用官方 CLI reference列出的 --bare,或 --settings '{"disableAllHooks":true}' 暫停非 managed Hooks;兩者只移除各自文件列出的輸入,不代表倉庫已可信。v2.1.248 另提供 --restricted,使用前仍要按該版本文件驗證限制。

Claude Code Hooks 教學 FAQ

1. Hook 可以取代 CLAUDE.md 嗎?

不行,兩者解決不同問題。CLAUDE.md 提供模型理解專案所需的背景與規範;Hook 處理特定事件上的自動化。常見組合是 CLAUDE.md 解釋「為什麼」,Hook 負責「何時執行」。

2. PostToolUse 失敗能撤銷剛才的編輯嗎?

不能。它在工具成功後才觸發。失敗訊息可以讓 Claude 修正,真正的回滾要靠 Git、transaction 或你自己的 backup 機制。

3. 為什麼不用 exit 1 來 block?

因為非零不等於通用 deny。多數 command Hook 的一般非零 exit 只會被視為非阻擋錯誤;PreToolUse 建議回現行結構化 decision,或在確定該事件支援時用 exit 2。

4. Hook timeout 會保護我嗎?

timeout 只限制等待,不等於安全決策。PreToolUse command timeout 目前會回正常 permissions 流程;所以 permissions deny/ask 必須獨立存在。

5. asyncRewake 適合跑測試嗎?

適合慢測試的通知,不適合作為唯一阻擋層。它會在背景執行,exit 2 可喚醒 Claude 處理失敗,但原動作已發生,後續編輯也可能同時進行。

6. PreToolUse 能完全保護 .env 嗎?

不能單獨做到。@.env 類參照不會觸發 PreToolUse;以 Read(.env)Edit(.env) deny 限制內建工具,再用 sandbox 約束 Bash 與其子程序。任意其他程序與 command Hook 本身仍要另外隔離。

7. 兩個 PostToolUse Hook 會照設定順序跑嗎?

不要依賴順序。目前所有匹配 Hook 平行執行。需要 formatter → test 的依賴,就放進一個 wrapper 內明確排序。

8. Hook 會省 token 或讓 Claude 更快嗎?

不能直接下結論。command Hook 本身在主 context 外執行,但錯誤與回饋可能進入對話,同步程序也增加 wall time。用配對任務實測完整 Session 用量、完成率與 p95 延遲才有答案。

給新手的 6 個重點

  1. 先寫清楚事件、條件、Handler,再決定是不是 Hook。
  2. command Hook 優先使用 commandargs 的 exec form。
  3. PreToolUse 做動態 decision;秘密檔案以 permissions/sandbox 分層兜底。
  4. PostToolUse 不能撤銷副作用,需要順序的 formatter/test 放同一 wrapper。
  5. 收據只留 allowlist 過的 metadata、hash、exit 與 duration,不保存任意子程序輸出。
  6. 升級 Claude Code、換 OS/shell 或改 settings 後,重跑 no-decision、ask、deny、timeout、missing-runtime 與 outside-path canary。

接著閱讀

左右滑動查看更多推薦

結語:先裝一扇能驗收的門

不要一次搬進十個 Hook。今天先在 fixture repo 裝好 risk_gate.py,讓安全命令無 decision 回到正常 permissions、遠端寫入 ask、根目錄強刪 deny;再接上單一 quality wrapper,確認 formatter → test → receipt 的順序。當你能主動拔掉 Python、製造 timeout、改到工作目錄外,仍清楚知道哪一層會接手,這三道閘門才算真的可維護。

若你想把這套事件思維擴充成完整 Agent 工作流,可到 AlphaLab 線上課程繼續練習:先讓一個 Hook 可讀、可測,並替它造成的變更準備獨立回滾機制,再談自動化規模。

ALPHALAB 社群

有問題?來 Telegram 聊

和 Terry、編輯、其他網友一起討論這篇文章。提問、分享觀點,回覆更即時。

加入 Telegram 討論

📩 訂閱 AlphaLab 電子報

每週最多三封:一封 Weekly 週報與最多兩封關鍵 Alpha Signal。

我們不會 spam,隨時可退訂。