我有一個負責 K8s 部署的 Skill,流程跑了幾個月都沒出事。直到有天我回頭看「確認」那一步到底怎麼寫的——一行
Confirm with user before proceeding,沒有任何機制保證模型會停下來。這讓我不太舒服。
問題的起點:一行自然語言
那個部署 Skill 的流程是:刷 token → dry-run → 確認 → 正式部署。
「確認」那步長這樣:
Step 4: Confirm with user before proceeding
大多數時候 Claude 會停下來問我要不要繼續。但「大多數時候」跟「保證」是兩回事。這行字就是一段自然語言,模型在 token generation 的時候會「盡量」遵守,但沒有任何 runtime 機制保證它一定會停。
我決定把手上所有 Skill 翻一遍,看看還有多少類似的情況。
翻了 35 個 Skills
我有兩組 Skills——14 個團隊共用的 repo skills,21 個自己的 personal skills。全部讀過一遍後,先按「有沒有破壞性操作」分類:
| 類型 | 數量 | 範例 |
|---|---|---|
| Read-only / Advisory | 21 | log 分析、code review、狀態查詢 |
| 有破壞性操作 | 14 | 部署、git push、改設定檔、設備指令 |
再看那 14 個有破壞性操作的 Skill 怎麼處理「執行前確認」:
| 做法 | 數量 |
|---|---|
| 什麼都沒有 | 8 |
自然語言(CHECKPOINT、STOP、Confirm with user) | 5 |
| 指定調用 AskUserQuestion tool | 1 |
14 個有破壞性操作的 Skill,8 個完全沒有 checkpoint,5 個靠一行自然語言。
這不只是我的問題。去 GitHub 搜公開的 Claude Code Skill,大家都在用同一套做法——自然語言告示牌:
- claude-code-starter-kit 的 incident-response skill 直接寫在 behavioral rules 裡:
**STOP at checkpoints** — wait for user confirmation before proceeding,每個 phase 結尾都有**CHECKPOINT**: Present triage summary. Wait for user to confirm before investigation. - claude-code-ultimate-guide 的 talk-pipeline skill 用
CHECKPOINT步驟搭配Do not invoke Stage 5 without explicit user confirmation,anti-patterns 裡還特別提醒 “Skipping the CHECKPOINT — it’s the pipeline’s most important control point” - awesome-claude-skills 收錄了 50+ 個 verified skills——翻了一輪,沒有任何一個用 runtime 機制做 checkpoint
不管叫 CHECKPOINT、STOP、WAIT、還是 Confirm with user,本質都是同一件事:一行自然語言指令,希望模型讀到後停下來。
但這類告示牌不是 100% 的。GitHub Issue #18454 記錄了一個案例:用戶在 CLAUDE.md 寫了 ⛔ MANDATORY SESSION START (DO NOT SKIP) 和 Wait for confirmation before proceeding,標了粗體、用了 emoji、加了大寫——模型承認讀到了,然後完全無視,一口氣改了 23 個檔案。
那唯一用了 AskUserQuestion 的那個呢?它是一個 sprint planning skill,在列完 stories 後用 AskUserQuestion 讓用戶確認。寫法是:
Use AskUserQuestion to confirm the stories:
- Question: "以上 stories 正確嗎?"
- Options:
- "Correct, proceed" → Continue to next step
- "Need changes" → Ask what to modify
我的第一個反應是:「這才是正確做法。AskUserQuestion 是 tool call,調用後 runtime 會強制暫停生成、等用戶回應。這是 hard constraint。」
試了兩週後發現,這個結論只對了一半。
AskUserQuestion 沒有想像中硬
值得停下來想一下:模型決定要不要調用 AskUserQuestion,跟決定要不要遵守 CHECKPOINT/WAIT/STOP,用的是同一套機制——token generation。
CHECKPOINT/WAIT: 概率性遵守 → 輸出文字等你
AskUserQuestion: 概率性調用 → (調用了的話) runtime 強制阻斷
第二步確實是 deterministic 的——一旦 tool call 發出,runtime 會暫停生成,呈現 UI,等用戶選擇。這部分有官方文檔支撐:
Execution remains paused until your callback returns, and the SDK only cancels the wait when the query itself is cancelled.
但第一步呢?模型「決定要不要發出 tool call」這一步,跟「決定要不要遵守 CHECKPOINT」一樣是概率性的。
這不是我猜的。GitHub Issue #19308 的標題直接寫著:
Claude systematically ignores Skill tool despite explicit BLOCKING REQUIREMENT instructions
Skill 裡用大寫粗體寫「你必須調用這個 tool」,模型照樣跳過。
所以 AskUserQuestion 比純自然語言好嗎?好——多了一層 runtime 保護。但它是 100% 嗎?不是。兩者的差別是 single-layer(純概率)vs two-layer(概率 + deterministic),而不是「soft vs hard」。
那什麼才是真正 100% 的?
查完官方文檔後,我找到三個不依賴模型「決定要不要遵守」的機制——它們在 runtime 層運作,模型無法繞過。
PreToolUse Hook
這是最強的。Hook 在 tool call 執行前攔截,你可以檢查指令內容然後決定放行或擋掉:
// .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": ["bash .claude/hooks/block-destructive.sh"]
}
]
}
}
# .claude/hooks/block-destructive.sh
INPUT=$(cat /dev/stdin)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -qE "git push|kubectl apply|kubectl delete|rm -rf"; then
cat <<EOF
{
"decision": "block",
"reason": "Blocked: $COMMAND — run manually if intended."
}
EOF
exit 0
fi
根據官方文檔,PreToolUse Hook 連 bypassPermissions 模式都擋得住:
A hook that returns
permissionDecision: "deny"blocks the tool even inbypassPermissionsmode or with--dangerously-skip-permissions.
模型嘗試 git push?Hook 在 shell 執行前攔截。模型沒辦法繞過,因為這整件事發生在模型的控制範圍之外。
Skill 拆分
把一個長流程 Skill 拆成兩個獨立的 Skill:
原本:/deploy → dry-run → confirm → deploy → verify
拆成:/deploy-prepare → dry-run → 輸出結果
/deploy-execute → 用戶手動觸發 → deploy → verify
用戶必須自己打 /deploy-execute。模型不會幫你觸發 user-invocable 的 Skill——這是 runtime 保證的。
disallowed-tools
---
name: log-analyzer
disallowed-tools:
- Edit
- Write
- Bash
---
disallowed-tools 在 Skill 啟用期間從模型的可用工具池中移除指定工具。模型看不到這些工具,自然不會調用。不過限制在用戶下一條訊息後清除,對分析類 Skill 夠用,對部署類不夠。
這裡有一個容易搞混的地方:allowed-tools 不是限制。 官方文檔明確寫著它只是 grant permission(預先授權),不會阻止模型調用清單外的工具。我最初也搞反了,查了文檔才修正。
三層防護模型
整理完之後,所有「讓模型在破壞性操作前停下來」的機制可以分成三層:
| 層級 | 做法 | 依賴模型遵守 | 可靠性 |
|---|---|---|---|
| 自然語言 | CHECKPOINT、WAIT、STOP、Confirm | 100% 依賴 | 概率性 |
| Tool call 指令 | Use AskUserQuestion | 調用決策依賴,執行不依賴 | 概率 + deterministic |
| Runtime 機制 | Hook、Skill 拆分、disallowed-tools | 0% 依賴 | 100% |
回到最初那個 K8s 部署 Skill,修正後的防護:
- Hook 攔截
kubectl apply——模型想跑也跑不了 - AskUserQuestion 在部署前呈現選項——正常流程的 UX
- 自然語言
IMPORTANT: Never deploy without approval——最後的 soft 防線
主防線在 Hook。即使 AskUserQuestion 被跳過(Issue #19308 說這會發生),kubectl apply 仍然被 Hook 擋住。AskUserQuestion 的價值不在安全性,在於它提供了比較好的用戶體驗(選項 UI)。
判斷框架
Skill 有不可逆操作嗎?
│
├── 沒有 → 不需要 checkpoint
│ 可選:disallowed-tools 移除寫入工具
│
└── 有 → Runtime 層做主防線
├── Hook 攔截危險命令(最靈活)
├── Skill 拆分:prepare + execute(最簡單)
└── 可選:疊加 AskUserQuestion 改善 UX
回頭看
35 個 Skill 翻完、官方文檔查完、GitHub Issues 讀完之後,最大的收穫不是「發現了三層模型」,而是意識到自己之前對 LLM 控制機制的直覺是錯的。
我以為「告訴模型調用一個 tool」比「告訴模型停下來等」更可靠。聽起來很合理——tool call 有 runtime 保護,自然語言沒有。但「模型決定要不要調用 tool」這一步本身就是概率性的,跟「模型決定要不要遵守 CHECKPOINT」用的是同一套機制。
一句話:如果某個行為的安全性取決於模型「決定遵守」你的指令,它就不是 100% 的。 100% 只存在於模型控制範圍之外的機制。
參考資料
- Claude Code Hooks Guide — PreToolUse Hook 的官方文檔
- Claude Code Skills — Skill frontmatter 定義(
allowed-tools、disallowed-tools) - Handle approvals and user input — AskUserQuestion 的 blocking 行為
- GitHub Issue #19308 — 模型忽略 Skill 中明確 tool call 指令的實例
- GitHub Issue #18454 — 模型忽略 CLAUDE.md 和 Skills 中標記 MANDATORY 的自然語言 checkpoint
- claude-code-starter-kit incident-response — 公開 Skill 使用
STOP at checkpoints的實例 - claude-code-ultimate-guide talk-pipeline — 公開 Skill 使用
CHECKPOINT的實例
這是「Claude Code 實戰」系列。上一篇:Git 作為 Claude Code 的外接大腦:超越 MEMORY.md 的記憶架構。