Hooks 系統
建構 PreToolUse、PostToolUse 和其他生命週期事件的事件訂閱者。
你將學到什麼
Claude Code 有內建的權限系統,但組織需要更多。安全團隊可能想阻止所有對生產環境設定檔的寫入。合規團隊可能需要記錄每個執行的命令的稽核日誌。平台團隊可能想自動標記被 AI 修改的檔案。Hooks 系統讓這一切成為可能。
完成後,你將了解:
- 什麼是 hooks 以及它們何時觸發
- 四種 hook 事件類型及其用途
- 如何在
settings.json中設定 hooks - matcher 模式如何篩選哪些工具觸發 hook
- 輸入/輸出契約(JSON stdin、exit codes)
- 如何建構安全閘道和稽核系統
問題是什麼
內建的權限系統是二元的:允許或拒絕。但現實世界的政策是有細微差異的:
- 「允許 Bash,但阻止任何包含
rm -rf的命令」 - 「允許檔案編輯,但不允許編輯
config/production/中的檔案」 - 「允許一切,但將每個動作記錄到稽核檔案」
- 「允許 git commit,但要求訊息中包含 ticket 編號」
你無法透過標準的權限 UI 表達這些規則。你需要可程式化的政策強制執行 — 在工具執行生命週期的特定時間點執行的程式碼,能夠檢查、修改或阻止動作。
Without hooks:
AI calls tool → Permission check (allow/deny) → Execute
With hooks:
AI calls tool → Permission check → PreToolUse hook
│
inspect, validate,
block if needed
│
▼
Execute tool
│
▼
PostToolUse hook
│
log, audit,
side effects
如何運作
Hook 類型
Claude Code 在四個生命週期事件點觸發 hooks:
┌──────────────────────────────────────────────────┐
│ Hook Lifecycle │
│ │
│ ┌─────────────┐ │
│ │ Notification │ Fires when AI produces a │
│ │ Hook │ notification (status messages) │
│ └─────────────┘ │
│ │
│ ┌─────────────┐ │
│ │ PreToolUse │ Fires BEFORE a tool executes │
│ │ Hook │ Can BLOCK the tool call │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Tool │ The actual tool execution │
│ │ Execution │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ PostToolUse │ Fires AFTER a tool executes │
│ │ Hook │ For logging and side effects │
│ └─────────────┘ │
│ │
│ ┌─────────────┐ │
│ │ Stop │ Fires when the agent loop │
│ │ Hook │ is about to end a turn │
│ └─────────────┘ │
│ │
└──────────────────────────────────────────────────┘
| Hook | 何時觸發 | 能否阻止? | 常見用途 |
|---|---|---|---|
| PreToolUse | 工具執行前 | 是 | 安全閘道、驗證 |
| PostToolUse | 工具執行後 | 否 | 日誌記錄、稽核、副作用 |
| Notification | 狀態訊息時 | 否 | 自訂通知 |
| Stop | 回合結束前 | 是 | 最終驗證、清理 |
設定
Hooks 定義在 .claude/settings.json(專案層級)或 ~/.claude/settings.json(使用者層級)中:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "/path/to/validate-command.sh"
},
{
"matcher": "Edit",
"command": "/path/to/check-file-path.sh"
}
],
"PostToolUse": [
{
"matcher": "*",
"command": "/path/to/audit-log.sh"
}
]
}
}
每個 hook 條目有:
- matcher — 哪個工具觸發這個 hook。使用工具名稱如
"Bash"或"*"匹配所有工具。 - command — 要執行的 shell 命令。這就是你的 hook 腳本。
Matcher 模式
matcher 決定哪些工具呼叫會觸發這個 hook:
Matcher Matches
──────── ────────────────────────────
"Bash" Only Bash tool calls
"Edit" Only Edit tool calls
"Read" Only Read tool calls
"mcp__*" All MCP tool calls
"*" Every tool call
多個 hooks 可以匹配同一個工具呼叫。它們按順序執行。如果任何 PreToolUse hook 阻止了呼叫,工具呼叫就會被停止。
輸入/輸出契約
當 hook 觸發時,Claude Code 將上下文作為 JSON 傳遞到 stdin,並從 exit code 讀取決定:
輸入 (stdin):
{
"hook_type": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/build-cache"
},
"session_id": "abc-123",
"message_id": "msg-456"
}
輸出 (exit code):
| Exit Code | 意義 | 效果 |
|---|---|---|
| 0 | 允許 | 工具呼叫正常繼續 |
| 2 | 阻止 | 工具呼叫被阻止,AI 被告知原因 |
| 其他 | 錯誤 | 視為允許(fail-open) |
Stdout(可選,用於 exit code 2):
阻止時,hook 可以將原因寫入 stdout。這個原因會顯示給 AI,讓它可以調整:
BLOCKED: Cannot delete directories outside of project root.
AI 會收到這個作為工具錯誤,並可以嘗試不同的方法。
執行流程(詳細)
以下是設定 PreToolUse hook 後的完整流程:
AI wants to run: Bash("npm install express")
│
▼
┌──────────────────┐
│ Permission Check │ Standard allow/deny
└────────┬─────────┘
│ (allowed)
▼
┌──────────────────┐
│ Matcher Check │ Does "Bash" match any hooks?
└────────┬─────────┘
│ (yes, matched)
▼
┌──────────────────┐
│ Run Hook Script │ Spawn process, pipe JSON to stdin
│ │
│ stdin: { │
│ "tool_name": │
│ "Bash", │
│ "tool_input": { │
│ "command": │
│ "npm install" │
│ } │
│ } │
└────────┬─────────┘
│
┌─────┴─────┐
│ │
exit 0 exit 2
(allow) (block)
│ │
▼ ▼
Execute Return error
tool to AI:
"BLOCKED: ..."
關鍵洞見
Hooks 是組織政策的擴展點。 它們讓你在不修改 AI 指令的情況下強制執行 AI 必須遵守的規則。
這個區別很重要。你可以在 CLAUDE.md 中寫「永遠不要刪除生產檔案」,AI 通常會遵守。但指令可以被覆蓋、在壓縮後被遺忘,或在邊界情況下被忽略。一個檢查檔案路徑的 PreToolUse hook 是確定性的 — 無論 AI 做出什麼決定,它都無法被繞過。
可以這樣理解:
| 層級 | 機制 | 保證 |
|---|---|---|
| CLAUDE.md 指令 | AI 閱讀並遵循 | 盡力而為(AI 判斷) |
| 權限系統 | 每個工具允許/拒絕 | 二元的,沒有細微差異 |
| Hooks | 每次呼叫的自訂程式碼 | 確定性、可程式化 |
Hooks 填補了「好好要求 AI」和「硬編碼到 Claude Code 中」之間的空隙。它們是政策強制執行層。
實作範例
範例 1:阻止危險命令
一個防止破壞性 shell 命令的 PreToolUse hook:
#!/bin/bash
# hooks/block-dangerous.sh
# PreToolUse hook for Bash tool
# Read JSON input from stdin
INPUT=$(cat)
# Extract the command
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Define blocked patterns
BLOCKED_PATTERNS=(
"rm -rf /"
"rm -rf ~"
"DROP DATABASE"
"DROP TABLE"
"mkfs"
"> /dev/sda"
"chmod -R 777 /"
)
# Check each pattern
for pattern in "${BLOCKED_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qi "$pattern"; then
echo "BLOCKED: Command contains dangerous pattern: $pattern"
exit 2
fi
done
# Allow the command
exit 0
範例 2:稽核日誌
一個記錄每次工具執行的 PostToolUse hook:
#!/bin/bash
# hooks/audit-log.sh
# PostToolUse hook for all tools
INPUT=$(cat)
# Extract fields
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
SESSION=$(echo "$INPUT" | jq -r '.session_id')
# Build log entry
LOG_ENTRY=$(jq -n \
--arg ts "$TIMESTAMP" \
--arg tool "$TOOL" \
--arg session "$SESSION" \
--argjson input "$(echo "$INPUT" | jq '.tool_input')" \
'{timestamp: $ts, tool: $tool, session: $session, input: $input}')
# Append to audit log
echo "$LOG_ENTRY" >> ~/.claude/audit.jsonl
# PostToolUse hooks always exit 0 (can't block after execution)
exit 0
這會產生一個 JSONL 稽核軌跡:
{"timestamp":"2026-03-14T10:00:01Z","tool":"Bash","session":"abc","input":{"command":"npm test"}}
{"timestamp":"2026-03-14T10:00:03Z","tool":"Edit","session":"abc","input":{"file_path":"src/auth.ts",...}}
{"timestamp":"2026-03-14T10:00:05Z","tool":"Read","session":"abc","input":{"file_path":"package.json"}}
範例 3:保護生產檔案
一個防止編輯生產設定的 PreToolUse hook:
#!/bin/bash
# hooks/protect-prod-config.sh
# PreToolUse hook for Edit and Write tools
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Define protected paths
PROTECTED=(
"config/production"
".env.production"
"docker-compose.prod"
"k8s/production"
)
for protected in "${PROTECTED[@]}"; do
if echo "$FILE_PATH" | grep -q "$protected"; then
echo "BLOCKED: Cannot modify production file: $FILE_PATH. Use a staging config instead."
exit 2
fi
done
exit 0
整合在一起
這是包含所有三個 hooks 的完整 settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "/project/.claude/hooks/block-dangerous.sh"
},
{
"matcher": "Edit",
"command": "/project/.claude/hooks/protect-prod-config.sh"
},
{
"matcher": "Write",
"command": "/project/.claude/hooks/protect-prod-config.sh"
}
],
"PostToolUse": [
{
"matcher": "*",
"command": "/project/.claude/hooks/audit-log.sh"
}
]
}
}
使用此設定:
- 每個 Bash 命令在執行前都會對照黑名單進行檢查
- 每次檔案編輯在執行前都會對照受保護路徑進行檢查
- 每次工具執行(所有工具)在執行後都會被記錄到稽核檔案
AI 不知道 hooks 的存在。它正常呼叫工具。如果 hook 阻止了呼叫,AI 會收到錯誤訊息並進行調整 — 通常是選擇一個更安全的替代方案。
前後對比
| 沒有 Hooks | 有 Hooks |
|---|---|
| 政策是指令(盡力而為) | 政策是程式碼(確定性) |
| 沒有稽核軌跡 | 每個動作都被記錄 |
| 保護依賴 AI 的遵從性 | 保護在執行時強制執行 |
| 所有專案相同的規則 | 按專案設定 hook |
| 無法與外部系統整合 | Hooks 可以呼叫任何外部 API |
| 安全性基於信任 | 安全性是先驗證再信任 |
下一堂課
這完成了模組 3:真實架構。你現在理解了讓 Claude Code 可擴展的三大支柱:控制協議(CLI 和 SDK 如何通訊)、MCP(外部工具如何接入)、和 hooks(政策如何被強制執行)。
第 16 堂課開始模組 4:設定與自訂,介紹 Session 儲存 — Claude Code 如何在 session 之間持久化狀態,實現連續性和記憶。