你明明在 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在工具執行前:本文命中規則時回ask或deny;未命中時 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)兜底。

官方 Hooks reference列出完整事件與輸入/輸出 schema;本文只實作 PreToolUse 與 PostToolUse。若你習慣把所有規則塞進一個檔案,可先看 CLAUDE.md 指令膨脹整理法,再把真正需要事件觸發的動作搬出來。
CLAUDE.md、Skill、Subagent、Hook 怎麼選?
Anthropic 在 2026 年 6 月公布的官方選擇指南把差異講得很直接:根目錄 CLAUDE.md 會留在 Session 脈絡;Skill 在需要時載入完整流程;Subagent 使用獨立 context,只把結果帶回;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:收到成功的 Edit/Write 後,先驗證檔案仍在目前工作目錄,再依序跑 Prettier 與 npm test。這個安全版刻意丟棄子程序 stdout/stderr 與 argv,不保存 tool_input.content 或 tool_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 腳本本身也拿到使用者權限。下面的窄範圍分類器只辨識本文列出的直接、頂層 rm/git 拼法,並把管線、背景執行、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,所以範例加入 Read/Edit 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

- 觸發覆蓋率:應觸發事件數和實際收據數是否一致;少一張就先查 matcher、路徑、執行器與 trust。
- 決策正確率:預先標註 no-decision/ask/deny,另行記錄工具最後是 allowed/prompted/denied,計算 false allow 與 false block。
- 品質結果:formatter exit、test exit、Claude 修復後是否回到綠燈;PostToolUse 失敗不等於原編輯已回滾。
- 延遲:看每個 Hook 與整體任務的 p50/p95,而非只拿一次最快結果。
- 脈絡成本: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 個重點
- 先寫清楚事件、條件、Handler,再決定是不是 Hook。
- command Hook 優先使用
command+args的 exec form。 - PreToolUse 做動態 decision;秘密檔案以 permissions/sandbox 分層兜底。
- PostToolUse 不能撤銷副作用,需要順序的 formatter/test 放同一 wrapper。
- 收據只留 allowlist 過的 metadata、hash、exit 與 duration,不保存任意子程序輸出。
- 升級 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 可讀、可測,並替它造成的變更準備獨立回滾機制,再談自動化規模。






