Claude Code Hooks 與權限封面:改檔後自動檢查、exit code 2 擋下危險指令、allow/ask/deny 判斷順序、沙箱範圍與第三方套件審查

Claude Code 的 hooks 是在固定時機自動執行的指令,例如改完檔案就跑格式化、執行指令前先檢查。權限規則用 allow、ask、deny 決定動作要免問、要先問,還是一律禁止。CLAUDE.md 裡的規則只是給 Claude 參考;要確定擋下,就用 PreToolUse hook 回傳 exit code 2,或寫 deny 規則。

本文的例子都套在一個假設的專案上:假設你在做一個記帳網站,叫做豪享記帳,前端是 Next.js、後端是 Go。權限模式與基本的 allow 寫法,請看Claude Code 是什麼?。

Hooks、權限和 CLAUDE.md 差在哪?

差別在「誰執行」。CLAUDE.md 和 skill 是寫給 Claude 看的說明,Claude 會參考,但不保證每次照做。權限規則與 hooks 由 Claude Code 程式執行,不靠模型自己記得。

四種管控方式比較

方式由誰執行能不能擋下動作適合做什麼
CLAUDE.md、skillClaude 自己參考不保證專案慣例、程式與寫作風格
權限規則Claude Code能,deny 直接拒絕工具與指令要免問、要問或禁止
hooksClaude Code 在固定時機執行你的指令PreToolUse 可以格式化、檢查、記錄、擋特定指令
沙箱作業系統能,限制檔案與網路Bash 指令只碰得到允許的範圍

資料來源:Claude Code 官方文件的權限與hooks 指南頁

判斷方法是:「希望 Claude 這樣做」寫進 CLAUDE.md;「一定要發生」或「一定不能發生」,交給 hooks 和權限規則。CLAUDE.md 的寫法請看CLAUDE.md 要寫什麼?。

Claude 每次要用工具,都會依序經過下面幾關:

一次工具呼叫的流程圖:Claude 提出動作,先經過執行前 hook,再比對權限規則,接著執行工具,最後經過執行後 hook 一次工具呼叫依序經過執行前 hook、權限規則、執行工具與執行後 hook

Hooks 怎麼設定?事件、matcher 與指令

Hooks 寫在設定檔的 hooks 鍵底下,結構是「事件 → matcher → 指令」。事件決定什麼時候執行,matcher 篩選工具名稱,指令就是要跑的 shell 命令。

常用的 hook 事件

事件什麼時候觸發官方範例的用途
SessionStart會話開始或恢復時壓縮對話後重新注入專案提醒
UserPromptSubmit送出提示、Claude 處理之前每次提示附上目前的分支狀態
PreToolUse工具執行之前擋下危險指令、保護特定檔案
PermissionRequest要跳出權限確認時自動核准特定的確認
PostToolUse工具執行成功之後改檔後跑格式化、記錄指令
NotificationClaude 等你輸入或確認時跳出桌面通知
StopClaude 回覆結束時檢查要求的工作是否都完成
SessionEnd會話結束時清掉暫存檔

資料來源:Claude Code 官方文件〈Automate actions with hooks〉

hooks 寫在哪裡,決定它影響的範圍:

  • ~/.claude/settings.json:你的所有專案,只在你的電腦。
  • .claude/settings.json:單一專案,可以提交到 repo 給團隊共用。
  • .claude/settings.local.json:單一專案,只給你自己。
  • managed 設定:整個組織,由管理員控管。
  • 外掛的 hooks/hooks.json:外掛啟用時生效。
  • skill 或子代理的 frontmatter:skill 被叫用後的整個會話,或子代理執行期間。

事件發生時,Claude Code 會把資料用 JSON 傳到腳本的 stdin。Bash 指令放在 tool_input.command 欄位。改檔的路徑放在 tool_input.file_path 欄位。

設定好之後,在 Claude Code 輸入 /hooks,可以看到所有 hook 依事件分組。

注意:command hook 用你的使用者權限執行,能讀寫你帳號碰得到的所有檔案。官方建議加入前先檢查、測試每一條指令。腳本裡的變數要加雙引號,也要檢查路徑裡的 ..,並避開 .env、.git/ 與金鑰。

改完檔案怎麼自動跑格式化?

用 PostToolUse 事件,matcher 寫 Edit|Write。Claude 每次改檔或寫檔之後,就會執行你的指令。豪享記帳的前端用 Prettier、後端用 gofmt,所以腳本依副檔名分開處理。

  1. 在專案建立 .claude/hooks/fmt.sh,內容如下方第一段程式碼。
  2. macOS 與 Linux 要加執行權限:chmod +x .claude/hooks/fmt.sh。
  3. 把下方第二段設定加進專案的 .claude/settings.json。
  4. 輸入 /hooks,確認 hook 出現在 PostToolUse 底下。
  5. 請 Claude 改一個 .go 檔,打開檔案確認已經排好格式。
bash
#!/bin/bash
# 改檔後依副檔名跑格式化
FILE_PATH=$(jq -r '.tool_input.file_path // empty')
case "$FILE_PATH" in
  *.go)
    gofmt -w "$FILE_PATH" ;;
  *.ts|*.tsx|*.js|*.jsx|*.css)
    cd "$CLAUDE_PROJECT_DIR/web"
    npx prettier --write "$FILE_PATH" ;;
esac
exit 0
json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/fmt.sh"
          }
        ]
      }
    ]
  }
}

範例用 jq 解析 JSON,要先安裝。$CLAUDE_PROJECT_DIR 指向專案根目錄,官方建議用它引用腳本。豪享記帳的 Prettier 裝在 web/ 底下,所以先切到那個資料夾。

PostToolUse 有兩個限制要知道:

  • 不能復原:工具已經執行完,exit code 2 只會把 stderr 的訊息給 Claude 看。
  • 只看得到檔案工具:Claude 用 Bash 指令改的檔案,不會觸發 Edit|Write。可以改用 FileChanged hook,也可以自己加一個 Stop hook,在回合結束時掃一次工作目錄。

Windows 上的 hook 預設用 Git Bash 執行,沒裝 Git Bash 時改用 PowerShell。用 PowerShell 寫的腳本,在 hook 加上 "shell": "powershell"。

危險指令怎麼擋?PreToolUse 與 exit code 2

在 PreToolUse 事件檢查 Claude 要執行的指令,符合危險樣式就用 exit code 2 結束。Claude Code 會擋下這次工具呼叫,並把 stderr 的訊息回給 Claude,讓它換別的做法。

豪享記帳不讓 Claude 強制推送、硬重設 git,也不讓它直接刪資料表。腳本存成 .claude/hooks/guard.sh,加上執行權限後登記到 PreToolUse:

bash
#!/bin/bash
# 執行 Bash 指令前檢查,符合危險樣式就擋下
COMMAND=$(jq -r '.tool_input.command // empty')
PATTERNS=(
  "git push --force" "git push -f"
  "git reset --hard" "drop table"
)
for pattern in "${PATTERNS[@]}"; do
  if echo "$COMMAND" | grep -qi -- "$pattern"; then
    echo "已擋下:豪享記帳不讓 Claude 執行「$pattern」" >&2
    exit 2
  fi
done
exit 0
json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard.sh"
          }
        ]
      }
    ]
  }
}

補充:hooks 和 permissions 是同一個設定檔裡並列的鍵。上一節的 PostToolUse 和這裡的 PreToolUse,放在同一個 hooks 物件裡。

hook 的結束碼決定接下來發生什麼:

  • 0:沒有異議。對 PreToolUse 來說這不等於核准,照一般的權限流程走。
  • 2:可以擋的事件會擋下動作,stderr 的訊息依事件回給 Claude 或顯示給你。
  • 其他非 0:多數事件視為非阻擋錯誤,動作照常進行。

exit code 2 在各事件的效果

事件能不能擋exit code 2 時
PreToolUse能擋下這次工具呼叫
UserPromptSubmit能擋下提示,不送給 Claude
Stop能不讓 Claude 停下,繼續工作
PostToolUse不能stderr 給 Claude 看,工具已經執行
SessionStart不能只把 stderr 顯示給你
PermissionRequest不採用要改用 JSON 的 decision 回覆

資料來源:Claude Code 官方文件〈Hooks reference〉

PreToolUse 也可以用 exit code 0 搭配 JSON 輸出,把理由一起回給 Claude。官方建議一個 hook 只選一種方式:

json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "資料表結構要用遷移檔修改"
  }
}

寫阻擋用的 hook 要注意什麼?

  • hook 壞掉時預設放行:路徑寫錯或腳本當掉,多數事件會照常執行動作。要改成擋下,加上 "onFailure": "block",需要 v2.1.295 以上。
  • if 篩選是盡力而為:"if": "Bash(git *)" 可以只在 git 指令時啟動 hook。嚴格的允許或禁止,官方建議用權限規則。
  • 比對文字擋不住所有寫法:git -C . push --force 就不含「git push --force」這串字。作業系統層級的限制要靠沙箱。
  • PowerShell 也要涵蓋:Claude 能用 PowerShell 工具時,也會用它跑指令。matcher 要寫成 Bash|PowerShell。

登記之前,先把範例 JSON 從 stdin 餵給腳本手動測試,最後一行印出的結束碼應該是 2:

bash
INPUT='{"tool_input":{"command":"git push --force"}}'
echo "$INPUT" | .claude/hooks/guard.sh
echo $?

PreToolUse hook 在權限模式檢查之前執行,每一種模式都會跑。回傳 deny 的 hook,連 bypassPermissions 模式都擋得住。反過來,hook 回傳 allow,也不能越過 deny 或 ask 規則。

權限規則怎麼判斷?allow、ask、deny 的順序

權限規則固定依 deny、ask、allow 的順序比對,第一個符合的規則決定結果。規則寫得多具體都不改變順序,所以寬的 deny 會蓋過窄的 allow。

權限規則判斷順序圖:先比對 deny 規則,符合就拒絕;再比對 ask 規則,符合就跳出確認;再比對 allow 規則,符合就直接執行;都不符合時依權限模式決定 權限規則依 deny、ask、allow 的順序比對,第一個符合的決定結果

豪享記帳的專案設定加上這幾條規則:

json
{
  "permissions": {
    "allow": [
      "Bash(npm run *)",
      "Bash(go test *)",
      "WebFetch(domain:nextjs.org)",
      "WebFetch(domain:go.dev)"
    ],
    "ask": [
      "Bash(git push *)",
      "Edit(/api/migrations/**)"
    ],
    "deny": [
      "Bash(git push --force *)",
      "Bash(curl *)",
      "Read(./secrets/**)"
    ]
  }
}
  • 強制推送:git push --force 同時符合 ask 和 deny,deny 先比對,所以直接拒絕。
  • 資料庫遷移檔:改 api/migrations/ 前一定要你確認。專案設定裡開頭的 / 代表專案根目錄。
  • curl:官方建議用 deny 擋掉 curl、wget。要抓網頁就改用 WebFetch,再指定允許的網域。

Bash 規則的萬用字元容易寫錯在哪?

  • * 前面的空格算數:Bash(ls *) 不包含 lsof,Bash(ls*) 會包含。
  • 複合指令每段分開比對:Bash(npm run *) 不會放行 npm run test && rm -rf dist。
  • Bash(*) 等於 Bash:兩種寫法都代表所有 Bash 指令。

檔案路徑規則怎麼寫?

Read 與 Edit 規則用 gitignore 的寫法,開頭的符號決定從哪裡算起:

寫法意思例子
//路徑從檔案系統根目錄算的絕對路徑Read(//home/dev/keys/**)
~/路徑從家目錄算Read(~/.ssh/**)
/路徑從設定檔的位置算,專案設定就是專案根目錄Edit(/api/migrations/**)
路徑 或 ./路徑從目前目錄算Read(./.env)

資料來源:Claude Code 官方文件〈Configure permissions〉

單一斜線開頭的 /Users/dev/file 不是絕對路徑,絕對路徑要寫兩條斜線。Windows 的路徑會先轉成 POSIX 格式,C:\Users\dev 要寫成 //c/Users/dev。

規則寫在不同設定檔,聽哪一個?

同一個鍵寫在多個檔案時,優先順序由高到低是 managed、命令列、本機、專案、使用者。permissions.allow 這類清單會合併,不會互相覆蓋。任何一層的 deny,其他層都放行不了。

專案設定的 allow 規則,要你在工作區信任對話框按下信任後才生效;deny 與 ask 只會限制動作,所以不受影響。輸入 /permissions,可以看到每條規則來自哪個設定檔。

權限規則擋不住哪些情況?

權限規則比對的是 Claude 寫出的指令文字,不是程式本身。官方列了幾種擋不住的情況:

  • 換個路徑呼叫:Bash(curl *) 擋不住 /usr/bin/curl 開頭的寫法。
  • 包在另一個 shell 裡:sh -c 'curl …' 也不會被比對到。
  • 程式自己開檔:Read(./.env) 管得到 cat 這類指令,管不到 Python 腳本自己開檔。

要讓限制不依賴指令文字,就要開沙箱。

沙箱能保護什麼?開啟方式與範圍

沙箱由作業系統限制 Bash 指令能碰的檔案與網路,預設關閉。在會話裡輸入 /sandbox 開啟,或在設定檔把 sandbox.enabled 設成 true。

沙箱支援 macOS、Linux 與 WSL2。原生 Windows 上,指令照常在沙箱外執行;要用沙箱,就把 Claude Code 放在 WSL2 裡跑。

沙箱開啟後的預設範圍

項目預設
寫入工作目錄、使用者暫存目錄,以及你加入的目錄
讀取大部分的電腦,包含 ~/.ssh 這類憑證檔
網路經過本機代理,只放行允許的網域,清單一開始是空的
環境變數沿用 Claude Code 的環境,包含裡面的密鑰

資料來源:Claude Code 官方文件〈Configure the sandboxed Bash tool〉

沙箱包住的是 Bash、PowerShell 與 Monitor 執行的指令,以及它們啟動的子程序。下面這些在沙箱外,用你的完整權限執行:

  • 內建的檔案與網頁工具:Read、Edit、WebFetch 照權限規則管。沙箱的 denyRead 擋不住 Read 工具。
  • Claude Code 啟動的其他程式:command hooks、本機的 MCP 伺服器、狀態列指令。

豪享記帳開啟沙箱,擋掉憑證目錄,網路只放行套件來源:

json
{
  "sandbox": {
    "enabled": true,
    "allowUnsandboxedCommands": false,
    "filesystem": {
      "denyRead": ["~/.ssh", "~/.aws"]
    },
    "network": {
      "allowedDomains": [
        "registry.npmjs.org",
        "proxy.golang.org",
        "sum.golang.org"
      ]
    }
  }
}

allowUnsandboxedCommands 設成 false,官方稱為嚴格沙箱模式。它會關掉「沙箱裡失敗後,改到沙箱外重跑」的退路。沙箱無法啟動時預設會照跑;要改成直接停止,把 sandbox.failIfUnavailable 設成 true。

注意:官方提醒,放行 github.com 這類範圍很大的網域,可能變成資料外流的管道。要把 hooks 與 MCP 伺服器也隔離,就把整個 Claude Code 放進容器或虛擬機。

第三方 skill、外掛與 MCP 有什麼風險?

風險分兩種:會在你電腦上執行的程式,以及會進入 Claude 脈絡的指示。官方文件說明,外掛可以用你的使用者權限執行任意程式。skill、指令與代理,則會改變 Claude 怎麼使用手上的工具。

第三方套件會帶進來什麼

內容會做的事要注意
外掛的 hooks在固定時機執行 shell 指令用你的權限跑,在沙箱外
MCP 伺服器啟動本機程式或連到遠端,提供工具抓外部內容時可能帶進提示注入
外掛的 bin/加進 Bash 的 PATHClaude 的指令可能執行裡面的程式
skill、指令、代理進入 Claude 的脈絡改變 Claude 做事的方式
自動更新在背景更新外掛審過的檔案可能被換掉

資料來源:Claude Code 官方文件的外掛安全與安全頁

幾個官方文件寫明的重點:

  • 權限與沙箱管不到外掛自己跑的程式:它們管的是 Claude 的工具呼叫。
  • MCP 伺服器沒有經過 Anthropic 的安全稽核:官方建議用自己寫的,或來自信任提供者的伺服器。
  • 專案 .mcp.json 的伺服器:互動模式會先問你,claude -p 與 Agent SDK 會直接載入。
  • 專案 skill 的 allowed-tools 與 hooks:預先放行的工具不受工作區信任限制。skill 的 hooks 在 claude -p 也會照用,clone 別人的 repo 要先看 .claude/。
  • 自動更新:外掛可能在背景自動更新,裝之前先確認它的更新設定。

在別人的 repo 跑 claude -p 要注意什麼?

claude -p 不會跳出信任對話框,會直接把資料夾當成已信任。repo 裡 .claude/settings.json 的 hooks,就會在你的電腦上執行。在不是自己寫的 repo 跑之前,官方建議先挑一種做法:

bash
# 不讀專案的 hooks、skill、子代理、外掛與 .mcp.json
claude --bare -p "列出目錄結構"
# 只關掉這一次的 hooks
claude -p --settings '{"disableAllHooks": true}' "列出目錄結構"

第三方 skill 怎麼用比較安全?先審查、再改寫

我的做法是:找到現成的 skill 或外掛,先不安裝,照四個步驟處理。我審過的幾個裡,有的會自動抓最新版,有的把權杖用明文存起來。我要的是裡面的知識,不是整包程式。

第三方 skill 處理流程圖:唯讀審查、萃取有出處的知識、用自己的話改寫成 skill、附上來源與授權 第三方 skill 先唯讀審查,再萃取知識、改寫成自己的版本

  1. 唯讀審查:看授權、版本與會執行的檔案。外掛看 hooks/hooks.json、.mcp.json、bin/;skill 看腳本與 frontmatter 的 allowed-tools、hooks。也確認它會不會自動更新、外傳資料。這一步可以交給子代理,並提醒它把內容當資料看。
  2. 萃取有出處的知識:留下有來源的做法,標出過時或沒有來源的說法。
  3. 用自己的話寫成 skill:用官方欄位 name、description,套上自己專案的情境。
  4. 附上來源與授權:記下 repo、授權與 commit。沒有授權檔的專案,只取觀念、不引用原文。

外掛安裝前,/plugin 的詳細資料會列出它要裝的指令、代理、skill、hooks 與伺服器。clone 下來的外掛,也可以不開會話就列出元件:

bash
claude --plugin-dir ./my-plugin plugin details my-plugin

豪享記帳從一份開源的 Go 審查清單萃取觀念,改寫成自己的 skill。檔案放在 .claude/skills/go-api-review/SKILL.md:

markdown
---
name: go-api-review
description: 審查豪享記帳 Go 後端的 API 改動。
when_to_use: 改到 api/ 底下的 handler 時使用。
---
# Go API 審查清單
1. 回給前端的錯誤訊息,不帶出資料庫的原始錯誤。
2. 寫入多張資料表的操作,包在同一個交易裡。
3. 金額用整數存最小單位,不用浮點數。
## 參考來源
| 來源 | 授權 | 版本 | 取用方式 |
|---|---|---|---|
| github.com/example/go-review-guide | MIT | commit 1a2b3c4 | 只取觀念,自己改寫 |

MCP 伺服器怎麼加比較安全?

  • 先在 local 範圍試:這是預設範圍,設定存在 ~/.claude.json,不進版本控制。
  • 權杖用環境變數帶入:.mcp.json 裡寫成 ${LEDGER_API_KEY},不寫死在檔案裡。
  • 不要的專案伺服器直接停用:把名稱加進 disabledMcpjsonServers,任何模式都不會載入。

舊的 hooks 與權限寫法要怎麼改?

我拿官方文件逐項對照自己早期的設定,下面幾處有更好的官方寫法。換成豪享記帳的情境如下:

hooks 與權限:舊寫法與新寫法對照

舊寫法問題新寫法
個人設定只寫 Bash(*),沒有 deny 規則與 hook所有 Bash 指令都免問,又沒有擋的機制改成具體的 allow 規則;要全部免問,就搭配擋特定指令的 PreToolUse hook
在原生 Windows 開放所有指令,以為有沙箱把關原生 Windows 不支援沙箱需要沙箱就改在 WSL2 跑,或縮小 allow 範圍
設定檔頂層寫 allowedTools,列 Task、LS 這類舊工具名設定檔的權限要寫在 permissions 底下,--allowedTools 是命令列參數;官方工具清單也沒有這些名稱寫進 permissions.allow,工具名照官方工具清單,派子代理的工具叫 Agent
規範寫「改完檔要跑格式化」「每次工具呼叫要留記錄」靠 Claude 記得,沒有程式執行寫成 PostToolUse hook
唯讀的審查代理設 bypassPermissions會跳過所有確認,官方只建議在隔離環境用用 tools 白名單限制成唯讀工具;工作目錄內的讀取本來就免問,CI 改用 dontAsk
第三方 skill 只審授權與程式碼漏看會自動執行與預先放行的部分加看 allowed-tools、frontmatter 的 hooks、.mcp.json、bin/ 與自動更新

資料來源:Claude Code 官方文件的權限、沙箱、工具清單與外掛安全頁

常見問題

Q1:Claude Code 的 hook 沒有執行,要怎麼檢查?

輸入 /hooks,確認 hook 出現在正確的事件底下;matcher 要和工具名稱完全一致,大小寫有分。設定檔是嚴格的 JSON,不能有註解或結尾逗號。腳本可以先用範例 JSON 從 stdin 餵進去,手動看結束碼。

Q2:hook 的 exit code 1 會擋下動作嗎?

不會。只有 exit code 2 會擋下,其他非 0 的結束碼是非阻擋錯誤,動作照常進行。想讓 hook 失敗時也擋下,在 command 或 http hook 加上 "onFailure": "block"。

Q3:Windows 可以用 Claude Code 的沙箱嗎?

原生 Windows 不支援,指令會在沙箱外照常執行。要用沙箱,把 Claude Code 裝在 WSL2 的 Linux 環境裡執行。

Q4:權限規則和 hook 的決定衝突時,聽誰的?

hook 用 exit code 2 或回傳 deny,會在權限規則之前擋下,就算有 allow 規則也一樣。反過來,hook 回傳 allow,不能越過 deny 或 ask 規則。

延伸閱讀