建置階段使用 hooks 作為防護機制,在沒有人為介入的情況下允許或封鎖動作(階段 3:建置)。hook 也可以詢問(ask),暫停動作直到執行該工作階段的人確認為止。當核准者是其他人,例如發布經理時,hook 會尋找他們已記錄的核准,並在核准存在之前封鎖該動作。
此做法位於階段 5:部署,因為發布關卡是最清楚的案例,但 hooks 並非僅限於部署:它們會在 Claude 執行動作的任何地方運作。例如,hooks 可以在階段 3:建置期間封鎖沒有變更單(change ticket)的遷移(migrations)與基礎設施(infra)編輯,並在階段 4:測試的修復任務期間阻止代理程式編輯測試檔案。
開始使用
- 先決條件:無。
- 基礎設施:變更流程所需核准的書面清單。
如何執行
- 工程領導層與變更管理及法規遵循部門共同列出必須保留的人工核准關卡,例如變更管理簽核、發布授權,以及對受保護路徑的編輯。
- 平台工程師將每個關卡表達為一個 hook,這是一個在 Claude 執行動作之前執行的腳本,可以允許(allow)、詢問(ask)或封鎖(block)。
- 團隊的 hooks 放在 Git 中的
.claude/settings.json,而不可協商的 hooks 則放在由平台或 IT 管理員擁有的受管理設定(managed settings)中,個別工程師無法將其關閉。 - 封鎖動作應該能自我說明,因此當 hook 阻止某個動作時,原因與核准途徑會出現在 Claude 的輸出中。
實際樣貌
此範例是一家保險公司的客戶入口網站,發布至開發(dev)、預備(staging)與生產(production)環境。部署透過 MCP 工具執行,每個環境各有一個伺服器,每個伺服器都有一個 release 工具。
下方的項目是專案 .claude/settings.json 中的獨立範例。其比對器(matcher)會攔截每個伺服器上的 release。空的 args 清單會讓 Claude Code 直接執行腳本,而不是透過 shell 執行:
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__deploy-.*__release",
"hooks": [
{ "type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/release-gate.sh",
"args": [] }
]
}
]
}
}關卡本身,.claude/hooks/release-gate.sh:
#!/bin/bash
# 發布關卡。dev:允許。staging:詢問。production:需有此版本的已核准變更單,否則封鎖。
input=$(cat)
tool=$(jq -r '.tool_name' <<<"$input")
version=$(jq -r '.tool_input.version' <<<"$input")
ticket=$(jq -r '.tool_input.ticket // empty' <<<"$input")
decide() { # decide <allow|ask|deny> <reason>
jq -n --arg decision "$1" --arg reason "$2" '{hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: $decision,
permissionDecisionReason: $reason}}'
exit 0
}
case "$tool" in
mcp__deploy-dev__release)
decide allow "dev needs no approval" ;;
mcp__deploy-staging__release)
[ "$GITHUB_REF" = "refs/heads/main" ] && decide allow "pipeline release from main"
decide ask "This ships $version to staging. Approve only if you own this release." ;;
mcp__deploy-prod__release)
route="Open a change ticket at https://change.example.com/new for version $version, \
wait for the release manager to approve it, then call release again with that ticket number."
[[ "$ticket" =~ ^CHG-[0-9]+$ && "$version" =~ ^[0-9a-f]{7,40}$ ]] ||
decide deny "Production needs an approved change ticket. $route"
if approver=$(/usr/local/bin/change-ticket show --ticket "$ticket" --format json |
jq -er --arg v "$version" 'select(.state == "approved" and .version == $v) | .approver' 2>/dev/null); then
decide allow "$ticket approved by $approver"
fi
decide deny "$ticket is not approved for version $version. $route" ;;
*)
decide deny "No release rule for $tool. Add one to release-gate.sh first." ;;
esac在腳本中,version 與 ticket 是 Claude 傳遞給 release 工具的輸入。change-ticket 代表您的變更系統自己的命令列工具。此關卡依賴於代理程式無法寫入它所讀取的記錄。
在非互動式執行中,沒有人可以回應 ask,因此 Claude Code 會拒絕該呼叫。這就是為什麼此關卡允許來自 main 分支管線的預備環境部署,因為已核准的合併(merge)即為核准。
此關卡有其限制。工程師可以在自己的工作階段中手動設定 GITHUB_REF,不過這樣做只會跳過他們本來會自己回答的提示。
ask 的原因會顯示給該人員。deny 的原因會顯示給 Claude,因此封鎖訊息會指明核准途徑,以便 Claude 轉達。預備環境的結果先出現,接著是沒有變更單的生產環境:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": "This ships 4f2c9e1 to staging. Approve only if you own this release."
}
}{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Production needs an approved change ticket. Open a change ticket at https://change.example.com/new for version 4f2c9e1, wait for the release manager to approve it, then call release again with that ticket number."
}
}治理考量
Hooks 就是核准關卡。關卡條件每次都會對每個人強制執行。允許與封鎖的決定都會附上時間戳記記錄下來。此關卡也定義了什麼算是核准,無論是已核准的變更單還是發布經理的簽核。
受監管企業的受管理設定
受監管企業的受管理設定,由平台團隊透過行動裝置管理(MDM)或管理員控制台部署。工程師無法編輯或覆寫其中的任何設定。請見下方:
{
"permissions": {
"deny": [
"Read(.env*)", "Read(./secrets/**)",
"WebFetch", "Bash(curl *)", "Bash(wget *)"
],
"allow": [
"Bash(git *)", "Bash(make build)",
"Bash(make test)", "Bash(make lint)"
],
"disableBypassPermissionsMode": "disable"
},
"allowManagedPermissionRulesOnly": true,
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false,
"network": { "allowedDomains": ["git.internal.example.com", "registry.npmjs.org"] },
"credentials": {
"files": [
{ "path": "~/.ssh", "mode": "deny" },
{ "path": "~/.aws/credentials", "mode": "deny" }
],
"envVars": [ { "name": "GITHUB_TOKEN", "mode": "deny" } ]
}
},
"allowManagedHooksOnly": true,
"disableSideloadFlags": true,
"allowManagedMcpServersOnly": true,
"strictKnownMarketplaces": [
{ "source": "github", "repo": "example-corp/approved-plugins" }
],
"requiredMinimumVersion": "2.1.193"
}這些設定在控制層面上的作用:
permissions.deny讓機密資訊不會進入代理程式的上下文,並封鎖透過工具進行的任意網路外連(egress)。permissions.allow預先核准安全的內部循環,使拒絕清單不會變成提示疲勞(prompt fatigue)。disableBypassPermissionsMode加上allowManagedPermissionRulesOnly意味著沒有任何工程師、專案檔案或命令列旗標可以放寬這些規則。sandbox涵蓋了權限無法涵蓋的部分。對 WebFetch 的工具層級拒絕並不能阻止 shell 命令連上網路,而作業系統層級的網域允許清單(allowlist)則會直接封鎖外連,因此這兩者在不同層級強制執行同一個目標。failIfUnavailable與allowUnsandboxedCommands將沙箱(sandbox)變成一項先決條件,意味著當沙箱無法初始化時,Claude Code 會拒絕啟動,而在沙箱內失敗的命令無法在沙箱外重試。credentials處理了拒絕規則未涵蓋的情況。permissions.deny管控 Claude 的檔案工具,但在沙箱中的 shell 命令預設仍可能讀取~/.ssh或~/.aws/credentials。此區塊拒絕這些讀取,並從沙箱化命令的環境中移除所列出的機密資訊。allowManagedHooksOnly意味著只有在受管理設定中定義的 hooks 才會執行;使用者、專案與本機設定中的 hooks 都會被封鎖,包括上方獨立的.claude/settings.json範例。為了讓此做法的核准關卡持續強制執行,請在受管理檔案自己的hooks區塊中定義它。disableSideloadFlags與strictKnownMarketplaces意味著工程師機器上的任何技能(skill)、代理程式、hook 或 MCP 伺服器,都必須透過組織核准的外掛市集(plugin marketplace)取得,而不是來自家目錄。市集允許清單控制可以安裝的內容,而那些會為單次執行側載(sideload)外掛、代理程式或 MCP 設定的旗標,會在啟動時被拒絕。allowManagedMcpServersOnly使代理程式的工具介面成為由平台團隊擁有的允許清單。requiredMinimumVersion會拒絕在低於核准下限的版本上啟動,因此這些控制措施是由組織實際評估過的建置版本強制執行的。
請將此範例視為起點,依您自己的環境進行客製化。每個拒絕規則都會移除某些能力,而適當的平衡取決於儲存庫的資料分類。設定參考文件(在新分頁中開啟)記載了所有鍵值,包括僅限受管理使用的鍵值。
如何衡量
針對 hooks 本身:
- 領先指標:在每個核准關卡上等待所花費的時間。每個 hook 決策都會連同時間戳記與允許或封鎖的判定結果寫入 OpenTelemetry 匯出資料,因此每個關卡的等待時間都是可見的。
- 落後指標:根據事件追蹤系統,在導入 hooks 前後,到達生產環境的關卡違規次數。