上一篇介紹 anthropics/oncall-kit 時,有一個設計我特別喜歡:setup 被拆成五個 Phase,而且每個 Phase 都要求 Human sign-off。
Phase 0 Discover
↓
Human Gate
↓
Phase 1 Mine
↓
Human Gate
↓
Phase 2 Interview
↓
Human Gate
↓
Phase 3 Validate
↓
Human Gate
↓
Phase 4 Install
oncall-setup/SKILL.md 明確要求,每一個 Phase 做完後都要停下來,等待使用者確認,不能直接進入下一個 Phase。Repo 的 CLAUDE.md 也再次規定:「Every setup phase ends at a gate」,而且不能因為使用者要求趕快做完,就一次執行兩個 Phase。
這是一個很漂亮的 Human Gate。
但上一篇寫完後,我還留下一個問題:
這個 Gate 是 Claude「被要求停下來」,還是 Claude Code runtime 真的會阻止它?
兩者其實不一樣。
oncall-kit 的做法大致是:
SKILL.md
↓
告訴 Claude 現在是哪個 Phase
↓
完成工作
↓
STOP
↓
等待 Human sign-off
↓
進下一個 Phase
這裡的 STOP 是寫給模型看的 instruction。
它不是 Claude Code 的特殊語法,也不代表 runtime 自動建立一個 lock。
也就是說,它比較接近:
「沒有核准,不要繼續。」
而不是:
「沒有核准,你就算想繼續也執行不了。」
oncall-kit 雖然也有 Hook,但目前 Repo 裡的 hooks/hooks.json 只有 SessionStart:
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/first-run.sh"
}
]
}
]
}
}
它的用途是 session 啟動時執行 first-run.sh,不是拿來鎖住 Phase。
所以比較準確的說法應該是:
oncall-kit 用 Skill + Human sign-off 做 workflow Gate;Claude Code 本身其實還能再加一層 runtime Gate,只是這個 Repo 沒有這樣實作。
而 Claude Code 最適合拿來做這件事的,就是 PreToolUse Hook。
Claude Code 的 Hook 會在 Agent 執行流程中的特定時間點被觸發。
其中:
PreToolUse
就是在 Tool 真正執行以前觸發,而且官方文件直接寫明:
Before a tool call executes. Can block it.
流程大概是:
Claude 決定呼叫 Tool
↓
PreToolUse
↓
檢查條件
↓
允許 / 拒絕
↓
Tool 才可能真正執行
例如 Claude 想執行:
mcp__routines__create
Claude Code 可以先跑一支檢查程式。
如果程式回傳:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Phase 4 尚未取得人工核准"
}
}
Claude Code 會直接 block 這次 Tool call。
官方文件自己的範例也是這樣做:在 PreToolUse 攔截 Bash,檢查到危險的 rm -rf 後回傳 permissionDecision: "deny";Claude Code 收到結果後會阻止 Tool 執行,並把拒絕原因交回 Claude。
這就和在 SKILL.md 寫:
請不要執行這個工具
有本質差異。
先說明:下面是延伸設計,不是 oncall-kit Repo 現有功能。
oncall-kit 原本的 Phase 4 並沒有提供一個 mcp__routines__create Tool。
它實際上的做法是讓 Claude 產生 routine,最後由 Human 把 routine 貼進 Slack。README 也明確說明 human 決定、human deploy,而且整套 on-call Agent 預設採 read-only。
但假設之後把它產品化,真的提供一個 Tool:
mcp__routines__create
讓 Agent 可以直接建立 routine。
我們就可以規定:
Phase 4 未核准
→ 可以產生 routine 草稿
→ 可以修改內容
→ 可以讓人 review
→ 但是不能真的 create
這時可以直接在專案的:
.claude/settings.json
設定:
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__routines__create",
"hooks": [
{
"type": "command",
"command": "python .claude/hooks/check-oncall-gate.py"
}
]
}
]
}
}
Claude Code 的 Hook 可以放在 .claude/settings.json,代表整個 project 都會使用;也可以放在 user settings、plugin 或 managed settings。
接著建立:
.claude/hooks/check-oncall-gate.py
最簡單的版本可以是:
import json
import os
import sys
event = json.load(sys.stdin)
approved = (
os.environ.get("ONCALL_PHASE4_APPROVED") == "1"
)
if (
event.get("tool_name") == "mcp__routines__create"
and not approved
):
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason":
"Phase 4 尚未取得人工核准"
}
}))
實際流程會變成:
Claude:
我要呼叫 mcp__routines__create
↓
Claude Code:
先觸發 PreToolUse
↓
check-oncall-gate.py
↓
Phase 4 approved?
↓
no yes
↓ ↓
deny 繼續一般
permission flow
這裡有一個很重要的細節。
Python Script 並不是「自己攔住 Tool」。
真正負責 enforcement 的仍然是 Claude Code runtime。
Script 只是回答:
這次 Tool call 可以過嗎?
然後 Claude Code 根據 Hook 結果決定是否執行。
官方文件也特別說明,如果 Hook 沒有輸出 decision,只是正常 exit 0,並不代表自動 approve,而是回到 Claude Code 原本的 permission flow。
上面的範例用了:
ONCALL_PHASE4_APPROVED=1
只是為了讓程式碼簡單。
真正做 production Gate 時,最重要的其實不是 Hook,而是:
誰有權改 approval?
假設我們把狀態存在:
{
"phase4_approved": false
}
然後 Claude 同時有權限修改這個檔案。
那 Gate 就沒有意義了。
因為可能變成:
Claude 發現 create 被擋
↓
修改 phase4_approved
false → true
↓
重新呼叫 create
這就像是:
門有上鎖,但是鑰匙也交給被鎖在門外的人。
所以正式環境比較合理的方式是:
Human
↓
Approval Service
↓
approved = true
而 Claude Code 的 Hook:
PreToolUse
↓
讀 approval state
↓
但沒有權限修改它
例如 approval 可以放在:
這才是真正的 Human Gate。
Claude Code 還有另一層:
permissions
例如:
{
"permissions": {
"deny": [
"mcp__production__delete"
]
}
}
這適合處理:
這個 Tool 永遠不應該給 Agent 使用。
Claude Code 的 permission 還有 allow、ask、deny,而且 deny 的優先權最高;只要任何 scope 有 deny,其他地方就不能再用 allow 把它打開。
所以可以簡單區分:
永遠不能做
→ permissions.deny
有條件才能做
→ PreToolUse
例如:
刪除 production DB
→ 永遠不能做
→ permissions.deny
建立 routine
→ Human approve 後才能做
→ PreToolUse
deploy production
→ change request approved 後才能做
→ PreToolUse
這樣就比全部塞進 CLAUDE.md 清楚很多。
Claude Code 現在其實支援直接在 Skill frontmatter 裡設定 Hook。
例如:
---
name: oncall-setup
hooks:
PreToolUse:
- matcher: "mcp__routines__create"
hooks:
- type: command
command: "python .claude/hooks/check-oncall-gate.py"
---
Skill 被 invoke 後,這個 Hook 會註冊,而且持續到該 session 結束。
所以技術上完全可以寫成:
oncall-setup Skill
├── SOP
├── Phase 定義
└── Gate Hook
很方便。
但是如果這是一個真正重要的 security policy,我反而不會只放 Skill。
因為 Skill Hook 的前提是:
這個 Skill 有被 invoke
假設 Claude 從其他 Skill、其他 prompt,甚至直接操作 Tool,就不一定受到這個 Skill-level Hook 保護。
如果規則是:
只要在這個專案裡,建立 routine 前都必須有人核准。
那比較適合放:
.claude/settings.json
如果規則是:
整間公司的 Claude Code 都不能繞過這個限制。
那就再往上放到:
Managed Settings
Claude Code 的設定有不同 scope:
~/.claude/settings.json
→ 個人
.claude/settings.json
→ Project
Managed Settings
→ Organization
而 Managed Settings 的優先權最高。
Claude Code 官方文件明確指出,管理員部署的 managed settings,user 與 project settings 不能覆蓋;managed permission deny 也不能被 --allowedTools 打開。
Hook 也有類似機制。
管理員可以設定:
allowManagedHooksOnly
讓 user、project、local 等一般 Hook 不再生效,只允許管理端控制的 Hook。
因此企業真正需要「不能自己拔掉的 Gate」時,可以變成:
Managed Settings
↓
PreToolUse
↓
Company approval service
↓
approved?
↓
no yes
↓ ↓
deny Tool
這就不是:
我們要求 Claude 記得不要做
而是:
公司把這條規則放在 Claude Code runtime 外圍
這裡還有一個很容易搞混的地方。
Claude Code 也有:
Stop
Hook。
看到 oncall-kit 裡一直寫:
STOP and wait for sign-off
很容易直覺認為應該用 Stop Hook。
其實不是。
Claude Code 的 Stop Hook 是:
Claude 準備結束這一輪回答
↓
觸發 Stop Hook
所以 block Stop 通常代表:
你還不能結束,繼續處理。
而不是:
你不能執行這個 Tool。
官方 lifecycle 也明確區分:PreToolUse 是 Tool 執行前,可以 block;Stop 則是在 Claude 完成 response 時觸發。
所以做 Human Gate,如果目的是:
未核准不得產生副作用
真正該擋的是:
PreToolUse
不是 Stop。