5 min read

Claude Code Skill 的安全閘門:從 35 個 Skills 的審計到三層防護模型

Table of Contents

我有一個負責 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 / Advisory21log 分析、code review、狀態查詢
有破壞性操作14部署、git push、改設定檔、設備指令

再看那 14 個有破壞性操作的 Skill 怎麼處理「執行前確認」:

做法數量
什麼都沒有8
自然語言(CHECKPOINTSTOPConfirm with user5
指定調用 AskUserQuestion tool1

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

不管叫 CHECKPOINTSTOPWAIT、還是 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 in bypassPermissions mode 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(預先授權),不會阻止模型調用清單外的工具。我最初也搞反了,查了文檔才修正。


三層防護模型

整理完之後,所有「讓模型在破壞性操作前停下來」的機制可以分成三層:

層級做法依賴模型遵守可靠性
自然語言CHECKPOINTWAITSTOPConfirm100% 依賴概率性
Tool call 指令Use AskUserQuestion調用決策依賴,執行不依賴概率 + deterministic
Runtime 機制Hook、Skill 拆分、disallowed-tools0% 依賴100%

回到最初那個 K8s 部署 Skill,修正後的防護:

  1. Hook 攔截 kubectl apply——模型想跑也跑不了
  2. AskUserQuestion 在部署前呈現選項——正常流程的 UX
  3. 自然語言 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 實戰」系列。上一篇:Git 作為 Claude Code 的外接大腦:超越 MEMORY.md 的記憶架構