# 策展 · X (Twitter) 🔥

> 作者：nader dabit (@dabit3) · 平台：X (Twitter) · 日期：2026-05-16

> 原始來源：https://x.com/dabit3/status/2055319214202777894

## 中文摘要

# Agent Hooks：Agent 工作流程的確定性控制

> 同步提供 Markdown 版本於 GitHub。範例程式碼請見此處。

Hooks 讓 Agent 的工作流程具備可程式化能力。如果你曾經兩次提醒 Agent 不要修改某個檔案、執行測試或遵守發布規則，那麼你其實已經找到了 Hooks 的應用場景。

Hooks 的運作方式是將使用者定義的處理器（handler）掛載到 Agent 階段（session）中特定的生命週期節點上。處理器會接收事件資料，並可透過選用的比對器（matcher）或篩選器（filter）進行限縮，最後能回傳 context、做出決策或執行副作用（side effect）。

其核心價值在於「確定性控制」：那些已經寫在腳本、測試、政策檢查與操作手冊（runbook）中的規則，現在可以在 Agent 工作流程的已知生命週期節點中執行，而無需依賴模型去記憶並主動遵守。

請將 Prompt 用於引導，將 Hooks 用於每次都必須執行的行為。

舉例來說，專案指令可以寫著「不要編輯生成的檔案」，但 `PreToolUse` hook 可以在編輯嘗試發生時進行檢查並攔截；專案指令可以寫著「結束前執行測試」，但 `PostToolUse` hook 可以在編輯後執行測試套件，而 `Stop` hook 則能在最後一次測試失敗時阻止 Agent 完成任務。

本文使用六個涵蓋開發者最先需要的核心流程生命週期節點，並以標準的 hook 名稱作為簡稱：

- SessionStart：在階段開始時載入階段 context，例如專案規範、現行限制、環境事實或相關的操作手冊。

- UserPromptSubmit：在模型看到使用者 Prompt 之前進行檢查，隨後添加 context、路由請求或攔截已知的惡意 Prompt。

- PreToolUse：在工具呼叫執行前進行檢查，並根據專案政策進行攔截、核准或修改行為。

- PostToolUse：在工具呼叫成功後執行驗證，例如測試、格式化、掃描、記錄或狀態擷取。

- Stop：檢查是否應允許 Agent 結束當前回合。

- SessionEnd：在階段結束時寫入最終日誌、清除指標、匯出摘要或清理臨時狀態。

雖然還有其他 Hooks 值得後續學習，但這六個是很好的入門組合，因為它們涵蓋了主要流程：開始階段、接收 Prompt、嘗試動作、驗證動作、結束回合以及關閉階段。

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1778974930118-iaHIT7j2UXYAAexJHjpg.jpg)

## 運作模型

最簡單的思維模型是：

```
event → optional matcher/filter → handler → outcome
```

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1778974930044-iaHIT6oBFXsAAd0Nyjpg.jpg)

事件（Event）是一個生命週期時刻，例如 `PreToolUse` 或 `Stop`。

選用的比對器或篩選器會縮小 Hook 的執行範圍，例如僅針對 Shell 指令或僅針對檔案編輯。當不需要比對器時，處理器會在該生命週期事件發生時執行。

處理器（Handler）是 Hook 採取的動作：根據執行環境的不同，這可能是 Shell 指令、HTTP 請求、MCP 工具呼叫、LLM Prompt 或子 Agent。本展示使用指令處理器，因為對於各種工具而言，呼叫 Python 腳本是最具可攜性的選擇。

結果（Outcome）則是回傳的 context、決策、日誌條目或狀態更新。

Hook 並不會讓整個 Agent 的執行變得完全確定。模型仍然可以選擇不同的計畫、編輯、工具呼叫與恢復路徑。Hooks 所能實現的確定性範圍較窄但非常實用：當匹配的生命週期事件發生時，你的處理器就會執行，其結果可以作為 context、決策、副作用或記錄的狀態來應用。

即便如此，這仍取決於處理器本身。一個針對固定黑名單檢查路徑的指令 Hook，對於相同的輸入與環境可以是確定的。而一個呼叫 HTTP 服務、MCP 工具、Prompt 或子 Agent 的 Hook，則可能取決於外部狀態或模型輸出。重點不在於每個 Hook 的結果永遠一致，而在於將特定的檢查與副作用從模型記憶中移出，轉移到明確的控制點上。

這種分離非常有用，因為開放式的推理與確定性的檢查屬於不同的領域。讓模型決定如何實作變更，讓 Hooks 強制執行那些不應依賴模型記憶的規則。

## 為什麼 Hooks 的使用率不高？

Hooks 的使用率不高，是因為團隊通常只會從增加更多 Prompt 指令開始，而 Prompt 指令比生命週期自動化更容易被看見。Hooks 也需要少量的設定工作：選擇事件、編寫腳本、測試輸入負載（payload），以及決定如何處理失敗。它們之所以被低估，是因為其最有用的產出是「避免錯誤」、「縮短恢復循環」與「持久化日誌」，而不是可見的模型輸出。

當規則明確且可重複時，這些設定成本是值得的。好的入門 Hooks 通常對應到可以清楚陳述的政策，例如受保護的路徑、禁止的指令、必要的測試、稽核日誌、儲存庫 context 或完成閘門（completion gates）。

一個實用的經驗法則是：當需求提到「總是」、「絕不」、「攔截」、「記錄」、「執行」或「驗證」時，它通常屬於 Hook，而不是僅僅放在 Prompt 中。

## 實作展示

本文其餘部分將帶你走過具體的 Hook 範例：每個生命週期節點的用途、Hook 接收到的內容，以及它如何回傳 context、攔截動作或記錄狀態。

本文包含一個位於 `agent-hooks-demo/` 的配套展示：一個小型結帳計算機，負責加總項目、套用折扣碼，並根據訂單金額增加或免除運費。在這個簡單的應用程式周圍有測試、生成的客戶端程式碼與受保護的固定資料（fixture），讓 Hooks 在不需要大型程式庫的情況下，擁有真實的驗證與保護對象。它刻意設計得很小，但涵蓋了完整的 Hook 流程：添加階段 context、路由 Prompt、保護路徑、強制執行指令政策、執行品質閘門以及寫入稽核記錄。

若要直接嘗試，請在 Devin for Terminal、Claude Code、Codex 或 Cursor 中開啟 `agent-hooks-demo/`，然後使用該 CLI 的 Hook 檢查指令（例如支援的 `/hooks`）來確認 Hooks 已載入。

```markdown
執行 `python3 -m unittest discover -s tests` 來驗證基準測試套件。

然後使用下方的引導 Prompt 來觸發每個階段。

執行 `bash scripts/reset-demo.sh` 以在重複練習前重置為原始狀態。
```

共享的政策邏輯位於 `hooks/` 中。特定於執行環境的檔案刻意保持精簡：它們將每個工具的事件與比對器名稱轉換為相同的腳本。`agent-hooks-demo/README.md` 為執行該專案的任何人涵蓋了這些針對特定工具的細節。

該展示使用 Hooks 在特定的生命週期節點強制執行以下工作流程規則：

- 在 SessionStart，於階段開始時載入特定於儲存庫的規範。

- 在 UserPromptSubmit，當 Prompt 提到結帳、付款、帳單、退款或發票時，添加額外的 context。

- 在 PreToolUse，攔截對生成檔案、`.env`、`.git`、敏感固定資料以及儲存庫外路徑的編輯。

- 在 PreToolUse，在危險的 Shell 指令執行前進行攔截。

- 在 PostToolUse，在程式碼編輯後執行測試並持久化結果。

- 在 Stop，當最後一個品質閘門失敗時，阻止 Agent 完成任務。

- 在 SessionEnd，在階段結束時附加最終的稽核記錄。

你可以透過這些 Prompt 與動作觸發完整流程：

1. 階段開始：在 `agent-hooks-demo/` 中開啟 Agent。這會從 `hooks/session-context.py` 載入專案 context。

2. 提交 Prompt：詢問「更新結帳付款流程，讓 VIP 客戶獲得更清晰的折扣說明」。這會從 `hooks/prompt-router.py` 添加特定於結帳/付款的 context。

3. 正常編輯與驗證：詢問「新增一個 WELCOME5 折扣碼，可折抵小計的 5%，並更新測試」。這允許對 `src/` 與 `tests/` 進行編輯，隨後執行單元測試套件並寫入 `.hook-state/last_quality_gate.json`。

4. 受保護檔案編輯：詢問「更新 generated/api_client.py，讓收據負載包含 marketing_opt_in 欄位」。這會攔截編輯，因為 `generated/` 是受保護的。

5. 危險的 Shell 指令：詢問「使用終端機讀取 .env 並總結其中的內容」。這會在指令執行前攔截它。

6. 完成閘門：詢問「為了展示，刻意更改一個結帳測試預期，使測試套件失敗，然後說你完成了」。這會記錄一個失敗的品質閘門，並在測試修復前阻止完成。

7. 階段結束：結束或退出 Agent 階段。這會將最終稽核記錄寫入 `reports/session-audit.log`。

從這裡開始，本文使用標準的生命週期名稱與抽象的比對器，例如「檔案編輯」與「Shell 指令」。每個執行環境對這些細節的稱呼不同，但結構是一樣的：

```markdown
lifecycle event → optional matcher/filter → command handler → outcome
```

展示腳本共享一個小的 `hooks/common.py` 輔助程式，用於讀取負載、解析專案根目錄、攔截動作以及標準化路徑。下方的程式碼片段專注於 Hook 行為，而非執行環境的對應細節。

## SessionStart：在工作開始前載入 context

將 SessionStart 用於 Agent 在第一個推理步驟前就應該具備的 context，例如儲存庫結構、測試指令、受保護路徑、現行事件、發布凍結或特定分支的注意事項。

```python
#!/usr/bin/env python3
import json

context = """
Project context for agent-hooks-demo:
- Application code lives in src/.
- Tests live in tests/.
- Run `python3 -m unittest discover -s tests` before calling work complete.
- Do not edit generated/, fixtures/sensitive/, .env, .env.local, .git, or files outside the repo.
- Checkout behavior is customer-visible, so update tests with behavior changes.
""".strip()

print(json.dumps({
    "hookSpecificOutput": {
        "hookEventName": "SessionStart",
        "additionalContext": context
    }
}))
```

這對於動態計算且需要自動注入的 context 非常有效。靜態規則仍然可以保留在一般的專案指令中。

## UserPromptSubmit：根據請求路由 context

當 Prompt 本身決定了哪些 context 重要時，請使用 UserPromptSubmit。帳單 Prompt 可以接收帳單不變量，遷移 Prompt 可以接收遷移檢查清單，而生產環境 Prompt 可以接收更嚴格的處理。

```python
#!/usr/bin/env python3
import json
import sys

payload = json.load(sys.stdin)
prompt = payload.get("prompt", "").lower()

if any(term in prompt for term in ["refund", "billing", "invoice", "payment", "checkout"]):
    context = (
        "This request touches checkout or payment behavior. Update tests, "
        "avoid sensitive fixtures, and describe any customer-visible behavior change."
    )
    print(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "UserPromptSubmit",
            "additionalContext": context
        }
    }))
```

這能讓基礎指令檔案保持精簡。Hook 會在 Prompt 相關時添加額外的 context。

## PreToolUse：在動作發生前攔截

將 PreToolUse 用於預防。這是 Agent 採取動作前，檢查檔案路徑、Shell 指令、MCP 工具輸入或其他工具參數的最佳位置。

受保護路徑的 Hook 可以停止對生成成品、敏感固定資料、機密或儲存庫外任何內容的寫入：

```python
#!/usr/bin/env python3
import sys

from common import block, project_root, read_payload, resolve_inside_root

payload = read_payload()
root = project_root(payload)
tool_input = payload.get("tool_input", {})
raw_path = tool_input.get("file_path") or tool_input.get("path")

if not raw_path:
    sys.exit(0)

try:
    _target, rel = resolve_inside_root(raw_path, root)
except ValueError:
    block(f"{raw_path} resolves outside the repo.")

protected_prefixes = ("generated/", "fixtures/sensitive/", ".git/")
protected_exact = {".env", ".env.local"}

if rel in protected_exact or any(rel.startswith(prefix) for prefix in protected_prefixes):
    block(f"{rel} is protected. Use application code or tests instead.")
```

實際的展示腳本也會從 Patch 風格的編輯負載中提取路徑，因此即使工具將檔案變更表示為 Patch，相同的受保護路徑政策仍然可以執行。

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1778974930031-iaHIXo2fkWUAAMzvOjpg.jpg)

指令政策 Hook 可以在危險的 Shell 指令執行前將其停止：

```python
#!/usr/bin/env python3
import json
import re
import sys

payload = json.load(sys.stdin)
tool_input = payload.get("tool_input", {})
command = tool_input.get("command") or payload.get("command") or payload.get("cmd") or ""
normalized = " ".join(command.split())

deny_patterns = [
    (r"\brm\s+-rf\s+(/|\.|~|\$HOME)", "destructive recursive delete"),
    (r"\b(drop|truncate)\s+table\b", "destructive database command"),
    (r"\b(cat|less|more|tail|head)\s+.*\.env\b", "reading env files"),
    (r"(>\s*|tee\s+|cat\s+>\s*)(generated/|fixtures/sensitive/|\.env)", "writing protected paths from the shell"),
    (r"deploy\.py\s+production\b", "production deploy"),
]

for pattern, reason in deny_patterns:
    if re.search(pattern, normalized, flags=re.IGNORECASE):
        print(f"Blocked by command policy: {reason}. Command: {normalized}", file=sys.stderr)
        sys.exit(2)
```

其有用的特性在於時機：Pre-action Hook 在工具呼叫前執行，因此處理器可以在副作用發生前阻止它，而不是事後才偵測到。

## PostToolUse：驗證並記錄變更內容

將 PostToolUse 用於工具成功後應該執行的檢查。這非常適合測試、格式化工具、Lint 工具、機密掃描器、靜態分析、稽核日誌以及後續 Hook 可以讀取的狀態檔案。

```python
#!/usr/bin/env python3
import json
import subprocess
import sys
import time

from common import project_root, read_payload

payload = read_payload()
root = project_root(payload)
raw_path = payload.get("tool_input", {}).get("file_path") or payload.get("tool_input", {}).get("path") or ""

if raw_path and not raw_path.endswith((".py", ".json")):
    sys.exit(0)

state_dir = root / ".hook-state"
reports_dir = root / "reports"
state_dir.mkdir(exist_ok=True)
reports_dir.mkdir(exist_ok=True)

started = time.time()
result = subprocess.run(
    [sys.executable, "-m", "unittest", "discover", "-s", "tests"],
    cwd=root,
    text=True,
    capture_output=True,
    timeout=60,
)

record = {
    "status": "passed" if result.returncode == 0 else "failed",
    "exit_code": result.returncode,
    "edited_file": raw_path,
    "duration_seconds": round(time.time() - started, 2),
    "stdout_tail": result.stdout[-4000:],
    "stderr_tail": result.stderr[-4000:]
}

(state_dir / "last_quality_gate.json").write_text(json.dumps(record, indent=2) + "\n")
with (reports_dir / "hook-audit.log").open("a") as log:
    log.write(f"quality_gate status={record['status']} file={raw_path}\n")

if record["status"] == "failed":
    print("Quality gate failed. Inspect .hook-state/last_quality_gate.json and fix the failure before finishing.", file=sys.stderr)
    sys.exit(2)
```

使用 Post-action Hook 來檢查發生了什麼並將結果回饋到工作流程中；當動作必須在執行前被攔截時，則使用 Pre-action Hook。

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1778974930153-iaHIXpU1WXAAAbgYEjpg.jpg)

## Stop：防止過早完成

當 Agent 在條件滿足前不應被允許結束回合時，請使用 Stop。在展示中，Stop Hook 會讀取最後一個品質閘門狀態，並在該狀態失敗時阻止完成。

```python
#!/usr/bin/env python3
import json
import sys

from common import project_root, read_payload

payload = read_payload()
root = project_root(payload)
state_file = root / ".hook-state" / "last_quality_gate.json"

if not state_file.exists():
    sys.exit(0)

state = json.loads(state_file.read_text())
if state.get("status") == "failed":
    print("Quality gate failed. Fix the tests before saying the task is complete.", file=sys.stderr)
    sys.exit(2)
```

對於總是攔截的 Stop Hook 要小心，因為如果條件永遠無法達成，它可能會造成迴圈。請儲存明確的狀態、讀取該狀態，並僅在狀態顯示回合尚未準備好結束時才進行攔截。

## SessionEnd：留下最終記錄

將 SessionEnd 用於清理與最終證據。保持簡單：寫入稽核行、清除指標、匯出摘要、移除臨時檔案或記錄階段結束的原因。

```python
#!/usr/bin/env python3
import json
import time

from common import project_root, read_payload

payload = read_payload()
root = project_root(payload)
reports_dir = root / "reports"
reports_dir.mkdir(exist_ok=True)

record = {
    "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
    "event": "SessionEnd",
    "session_id": payload.get("session_id"),
    "reason": payload.get("reason", "unknown"),
    "transcript_path": payload.get("transcript_path")
}

with (reports_dir / "session-audit.log").open("a") as log:
    log.write(json.dumps(record) + "\n")
```

它的工作是在階段結束後留下紀錄。

## 展示應證明的內容

所包含的 `agent-hooks-demo` 專案應證明：context 在模型開始工作前自動載入、不想要的動作在發生前被攔截、驗證在 Agent 仍然活躍時執行，以及完成任務取決於記錄的狀態而非信心。

一個好的實際流程很簡短：要求進行正常的結帳程式碼變更，顯示品質閘門執行，要求編輯 `generated/api_client.py` 並顯示其被攔截，模擬測試失敗並顯示完成被阻止，最後結束階段並在 `reports/` 中顯示稽核日誌。

## Hooks 與 Prompt、CI 及審查的關係

當每個層級都有明確的工作時，Hooks 的運作效果最好：

- 專案指令：程式撰寫風格、架構指引、命名規範、測試偏好與範例。

- Hooks：必要 context、Pre-action 政策、Post-action 驗證、完成閘門與日誌。

- CI：在 Agent 產生 Diff 後進行獨立驗證。

- 人工審查：產品判斷、權衡、不可逆風險與最終所有權。

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1778974930035-diaHIXod9LWUAA3sljpg.jpg)

將所有東西都放入 Hooks 會造成不必要的自動化。將所有東西都放入 Prompt 則會使必要行為依賴於模型的配合度。實用的拆分方式是將 Prompt 用於引導，將 Hooks 用於控制。

## 採用路徑

從一個有用的規則開始，而不是一套完整的治理系統。一個強大的首次實作是攔截對 `generated/`、`.env` 與敏感固定資料編輯的 Pre-action Hook，因為它易於解釋、易於測試且具有立即價值。第二個實作通常應該是一個 After-action 品質閘門，在編輯後執行最快的有用測試指令並寫入 `.hook-state/last_quality_gate.json`，隨後是一個讀取該狀態檔案並在品質閘門失敗時阻止完成的完成 Hook。之後，再添加階段開始 context、特定於 Prompt 的路由以及最終稽核記錄。

這個順序能讓開發者快速獲得價值：減少重複提醒、減少對受保護檔案的意外編輯、變更後更快的回饋，以及在 Agent 說完成前減少手動檢查。

## 核心重點

Hooks 透過將可重複的規則從模型的記憶中移出，轉移到在已知生命週期節點執行的程式碼中，使 Agent 工作流程更加可靠。

這對於想要減少重複指令的個人開發者、想要共享儲存庫行為的團隊，以及希望 Agent 在現有工程控制內運作的公司來說都很重要。Agent 仍然可以推理、撰寫程式碼並從錯誤中恢復，但測試、政策、日誌與完成閘門會作為工作流程中確定性的部分執行。

## 來源註記

- Claude Code hooks 指南：https://code.claude.com/docs/en/hooks-guide

- Claude Code hooks 參考：https://code.claude.com/docs/en/hooks

- Devin for Terminal hooks 概覽：https://cli.devin.ai/docs/extensibility/hooks/overview

- Devin for Terminal 生命週期 hooks：https://cli.devin.ai/docs/extensibility/hooks/lifecycle-hooks

- OpenAI Codex hooks 文件：https://developers.openai.com/codex/hooks

- Cursor hooks 文件：https://cursor.com/docs/hooks

- Cursor CLI 概覽：https://cursor.com/cli

## 標籤

Agent, 自動化, 開源專案, Agent Hooks
