SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-16

Claude Code Hooks 設定與 exit code 2 完整解析

了解 Claude Code hook 為何不受模型同意影響,查看設定位置、事件觸發時機,以及 exit code 2 如何在工具呼叫前取消操作並回傳 stderr 原因。

Claude Code hook 是什麼

Claude Code hook 是 Claude Code 在自身生命週期的固定時間點自動執行的 shell 命令。這就是 hook 與規則檔案的根本差異。CLAUDE.md 中的指示是建議,模型會根據上下文中的其他內容衡量是否採用。hook 則是程式碼,不論模型是否同意,都會執行。如果你的 agent 一直略過你已經提醒過 2 次的 formatter,就不需要更強硬的指示,而需要使用 hook。

這個機制很簡單。你可以在設定檔中,於事件名稱下註冊命令。事件觸發時,Claude Code 會執行該命令,並將事件資料以 JSON(JavaScript object notation)格式寫入其標準輸入(stdin)。你的命令讀取這些資料、執行工作,然後以結束狀態回應。PreToolUse hook 回傳 exit 2 時,會在工具呼叫執行前取消該呼叫;你的 script 寫入標準錯誤(stderr)的內容,則會傳回模型,作為取消原因。

這裡的事件名稱與欄位名稱取自 Claude Code hooks 參考文件,並於 2026 年 8 月針對 release 2.1.232 進行確認。這個介面變動很快,因此在複製任何 blog post 中的 JSON 前,請先查看自己所用版本的參考文件,包括本文。使用 claude --version 列印目前版本。

Hook 設定所在位置

Hook 是設定檔中的 JSON 區塊。共有 6 個位置可以放置 Hook,而設定檔的適用範圍就是 Hook 的適用範圍。

  • ~/.claude/settings.json:適用於你電腦上的所有專案,不會影響其他人的設定。
  • .claude/settings.json:適用於單一專案,並提交至 repository,因此所有 clone 該專案的人都會取得此 Hook。
  • .claude/settings.local.json:適用於單一專案,但只在你的電腦上生效。
  • Managed policy settings:由管理員設定,適用於整個組織。
  • hooks/hooks.json:位於 plugin 內,只要該 plugin 啟用就會生效。
  • Skill 或 subagent 的 frontmatter:只要該元件處於啟用狀態就會生效。

這些檔案中的 Hook 項目會合併,而不是互相覆寫。專案設定檔會將其中的 Hook 加入 user settings 中的 Hook,而不是取代它們。因此,同一個事件可以包含來自多個檔案的多個 Hook。設定 "disableAllHooks": true 會停用這些 Hook,但有一個例外:來自 managed policy settings 的 Hook 仍會執行,除非同時在 managed settings 中套用該設定。

在工作階段中執行 /hooks,即可列出目前已註冊的所有 Hook,並依事件分組,顯示每個 Hook 的來源檔案與 matcher。此選單只能讀取,因此必須編輯設定檔才能修改 Hook。檔案監看程式通常會自動偵測變更,無須重新啟動。

Claude Code 有哪些 hook 事件

Release 2.1.232 列出 31 個事件,範圍從 SessionStartSessionEnd,涵蓋壓縮、子代理、worktree 與設定檔。伺服器管理通常只會用到其中幾個。

  • PreToolUse:工具呼叫執行前觸發。這是可以阻擋執行的事件。
  • PostToolUse:工具呼叫成功後觸發。工具呼叫失敗時則觸發 PostToolUseFailure;因此,必須掌握所有結果的 hook 需要同時使用這兩個事件。
  • PermissionRequest:工具呼叫需要權限決策時觸發,也就是即將顯示核准提示的時機。
  • UserPromptSubmit:提交提示後、Claude 處理提示前觸發。這個 hook 寫入 stdout 的內容會加入模型的 context。
  • SessionStartSessionEnd:分別在工作階段開始與結束時觸發。壓縮完成後也會觸發 SessionStart,此時 matcher 值為 compact
  • Stop:Claude 完成回應時觸發。每個回合只觸發一次,不是每項工作完成時觸發一次。

每個群組都有 matcher,用來決定哪些事件會執行 hook。在工具事件中,它會依工具名稱篩選,因此 "Edit|Write" 只會在檔案編輯時觸發,不會在其他事件觸發。Matchers 區分大小寫。空白 matcher 會在每次事件發生時觸發。來自 MCP(model context protocol)伺服器的工具名稱為 mcp__<server>__<tool>,因此使用 "mcp__github__.*" 作為 matcher,就能只比對其中一部伺服器的工具,不會比對其他伺服器。

Stop hook 有一項注意事項,應在撰寫前先了解。會阻擋執行的 Stop hook 會讓模型重新執行工作;連續阻擋 8 次後,Claude Code 會覆寫該 hook。請讀取 hook 輸入中的 stop_hook_active 欄位;當其值為 true 時結束並回傳 0,否則 hook 會持續迴圈,直到達到上限。

Hook 從標準輸入接收的內容

當 Claude 即將執行 npm test 時,Bash 上的 PreToolUse hook 會從標準輸入讀取以下內容:

{
  "session_id": "abc123",
  "cwd": "/home/deploy/myproject",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test"
  }
}

每個事件都包含 session_idcwdpermission_modetranscript_pathhook_event_name。工具事件還會加入 tool_nametool_inputtool_use_id。其他事件則包含各自的欄位:UserPromptSubmit 會取得 prompt 文字,而 SessionStart 會取得由 startupresumeclearcompactfork 組成的 source

在 shell script 中,通常使用 jq 讀取這些內容;精簡的伺服器映像檔通常未預先安裝此工具。請先在 Ubuntu 和 Debian 上使用 sudo apt install -y jq 安裝。

執行狀態碼對進行中工具呼叫的影響

共有 3 種結果。

  • Exit 0 表示 hook 未提出異議。在 PreToolUse 中,這不等同於核准,正常的權限流程仍會執行。在 UserPromptSubmitSessionStart 中,stdout 會加入模型的上下文。
  • Exit 2 會阻擋可阻擋的事件,包括 PreToolUse,而 stderr 會成為顯示給模型的原因。對於無法阻擋的事件,例如 PostToolUse,阻擋會被忽略,但 stderr 仍會作為回饋傳送給模型。
  • 任何其他執行狀態碼 都代表不會阻擋動作的錯誤。動作會繼續執行。逐字稿會顯示 hook 錯誤通知,其中包含 stderr 的第一行,並接在文字 Failed with non-blocking status code: 之後。

如果需要執行阻擋或保持靜默以外的動作,請使用 exit 0,並改為將 JSON 物件輸出至 stdout。PreToolUse hook 會使用 permissionDecision 做出決定:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Database drops go through a migration, not through the agent."
  }
}

"allow" 會略過互動式提示,"deny" 會取消呼叫並將原因傳送給模型,"ask" 則會照常顯示提示。每個 hook 應選擇一種風格。若將 exit 2 與 stdout 中的 JSON 決策混用,會得到必須自行查找的結果。

多個 hook 符合同一事件時,會平行執行,且每個 hook 都會執行至完成。一個 hook 的 deny 不會停止其他 hook,因此 logging hook 仍會寫入記錄,而 guardrail hook 同時拒絕相同的呼叫。接著 Claude Code 會合併各項回應,並依 deny、defer、ask、allow 的順序保留限制最嚴格的結果。

範例 1:在破壞性命令執行前封鎖它

將以下內容儲存為 .claude/hooks/block-destructive.sh,放在專案中:

#!/bin/bash
# Deny a Bash tool call whose command matches a banned pattern.
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

for pattern in 'rm -rf /' 'mkfs' 'dd if=' 'DROP TABLE'; do
  if printf '%s' "$COMMAND" | grep -qiF -- "$pattern"; then
    echo "Blocked by policy: the command matches '$pattern'. A human runs this one." >&2
    exit 2
  fi
done

exit 0

將其設為可執行檔,然後在 PreToolUse.claude/settings.json 中註冊:

chmod +x .claude/hooks/block-destructive.sh
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh",
            "timeout": 10,
            "statusMessage": "Checking the command against policy"
          }
        ]
      }
    ]
  }
}

在信任此腳本前,先手動測試它。因為 hook 若因自己的輸入而崩潰,就會失效開放:

echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /var/lib/postgresql"}}' \
  | .claude/hooks/block-destructive.sh
echo $?

你應會在 stderr 看到 Blocked by policy: 行,且結束代碼為 2。輸入無害的命令,例如 ls -la,應不會看到任何輸出,且結束代碼為 0。在工作階段中,被拒絕的呼叫會出現在逐字記錄中,並以你的訊息作為原因;model 會讀取該訊息並調整行為。

有一項特性使這項作法值得採用:PreToolUse hook 會在權限模式檢查前觸發,且適用於所有權限模式,因此即使在 bypassPermissions 下,deny 仍然有效。這也是 hook 適合搭配 Claude Code 自動模式及其權限設定 使用的原因;即使降低提示頻率,hook 仍會觸發。

必須清楚了解這項作法的限制。針對命令字串進行模式比對,只能在 agent 粗心時提供防護。它無法防止 agent 刻意規避,因為相同的命令可能以 grep 看不到的形式撰寫。嚴格規則應放在權限系統中,並由執行該程序的帳號加以限制。

每次編輯後進行格式化與 lint

PostToolUse 搭配 Edit|Write matcher,會在任何檔案編輯工具執行後運作。將以下內容儲存為 .claude/hooks/after-edit.sh

#!/bin/bash
# Format the edited file, then report lint failures back to the model.
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0

case "$FILE" in
  *.py)
    ruff format "$FILE" >/dev/null 2>&1
    if ! ruff check "$FILE" >&2; then
      exit 2
    fi
    ;;
  *.sh)
    if ! shellcheck "$FILE" >&2; then
      exit 2
    fi
    ;;
esac

exit 0
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/after-edit.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

請 Claude 將縮排錯誤的函式加入 Python 檔案,然後開啟該檔案。檔案會以格式化後的狀態返回。這就能確認 hook 已執行,因為 hook 成功時不會在對話中顯示任何內容。

這裡的 exit 2 不會復原任何變更。PostToolUse 會在工具執行完畢後觸發,因此無論如何,編輯內容都已寫入磁碟。exit 2 的作用是讓 ruff check 輸出以回饋形式傳給模型,使模型修正剛才引入的錯誤,而不是繼續執行。這就是在 commit 時才發現 lint 失敗,與 agent 在同一回合中修正錯誤之間的差異。

這裡有兩項 matcher 限制需要注意。Edit|Write 看不到由 shell command 變更的檔案,而 Claude 經常透過 Bash 寫入檔案,因此這項限制確實會造成遺漏。若要涵蓋每次呼叫,請同時比對 Bash,並讓 script 使用 git status --porcelain 列出變更的檔案。若要每回合執行一次,則改將掃描放在 Stop hook 中。

範例 3:記錄每次工具呼叫以供稽核

PostToolUse 上的空白比對條件會套用至每個工具。將記錄寫入 system journal,而不是寫入 home directory 中的檔案,可避免 agent 自己的 shell 存取這些記錄:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "jq -c '{time: now|todate, session: .session_id, cwd: .cwd, tool: .tool_name, input: .tool_input}' | logger -t claude-code -p local0.info"
          }
        ]
      }
    ]
  }
}

使用 journalctl -t claude-code -o cat | tail -n 5 讀取記錄。每次工具呼叫應各顯示一行 JSON,最新記錄位於最後。若完全沒有輸出,表示 hook 未執行;下方的疑難排解章節會說明相關檢查方式。

PostToolUseFailure 下方加入相同區塊,以記錄失敗的呼叫,因為 PostToolUse 只會在成功時執行,而失敗的命令通常才是需要關注的項目。使用 logger,而不是將內容附加至 home directory 中的檔案,原因在於檔案擁有權:hook 會以與 agent shell 相同的使用者身分執行,因此該使用者能附加內容的任何檔案,也能將其截斷。journal 則由 systemd-journald 以自身的帳戶寫入。

Hook 可執行多久

ChartDefault hook timeout in seconds, by hook type and event
The data behind this chart
[
  {
    "label": "command, http or mcp_tool hook",
    "default_timeout_seconds": 600
  },
  {
    "label": "agent hook",
    "default_timeout_seconds": 60
  },
  {
    "label": "prompt hook",
    "default_timeout_seconds": 30
  },
  {
    "label": "command hook on UserPromptSubmit",
    "default_timeout_seconds": 30
  },
  {
    "label": "command hook on MessageDisplay",
    "default_timeout_seconds": 10
  },
  {
    "label": "any hook on SessionEnd",
    "default_timeout_seconds": 1.5
  }
]

命令 hook 預設可執行 600 秒,也就是 10 分鐘。部分事件會大幅縮短這段時間。SessionEnd hook 共用 1.5 秒的總額度,因此工作階段結束時的清理作業必須快速完成;不過,在 hook 上設定較長的 timeout,即可將這項共用額度提高到相同秒數,最高為 60 秒。

超過逾時時間的 hook 會遭到取消,且不會產生任何判定。對 PreToolUse guardrail 而言,這表示它不會阻擋操作:工具呼叫會繼續進入一般權限流程。因此,guardrail script 應保持精簡。對於無須讓使用者等待的慢速工作,例如將 log 傳送到其他位置,請設定 "async": true,讓 hook 在背景執行,而不會暫停工具呼叫。

Hooks、rules 檔案、skills 與 MCP servers

這4種機制都會改變 agent 的行為,因此容易混淆。但只有其中1種不只是建議。

rules 檔案(CLAUDE.md,或 .claude/rules/ 下的檔案)是載入 model context 的文字。它會影響行為,但不會強制執行任何規則。當對話很長、diff 很大,且使用者提出新的請求時,其中某一行可能被忽略。這就是 agent 忽略你寫下的指示 的一般原因。

skill 是由指示與 script 組成的資料夾,model 判斷相關時才會載入。這個判斷正是 skill 的作用,也是它的限制:仍由 model 決定是否使用。以 Ponytail,它會引導 agent 採用能運作的最小變更 為例,skill 能影響整個工作的處理方式,這是 hook 無法做到的;但只有在 model 選擇載入它時才會生效。

MCP(model context protocol)server 會提供 model 可呼叫的新工具,擴大 agent 能夠存取的範圍。但它不會讓 agent 主動使用任何工具,而且是你必須自行操作的獨立程序;這本身就是另一項工作,請參閱 在 VPS 上執行 MCP servers

Hook 是4種機制中唯一不需要 model 選擇就會執行的機制。偏好使用 rules 檔案,需要 model 在適用時遵循的程序則使用 skill。對於每次都必須執行的步驟,或絕對不能發生的事情,請使用 hook。更深入的比較,包括 skill 何時優於 rules 檔案,請參閱 skills、MCP 與 rules 檔案的比較

Plugin 是封裝方式,不是第5種機制。它會將 hooks 與 skills 綁定成一個可安裝的單位,讓團隊能將相同的防護措施部署到每台機器;請參閱 Claude Code plugins 的運作方式

共享 VPS 上的安全決策

Hook 是由 agent 觸發並執行的程式碼,執行身分與啟動 Claude Code 的使用者相同。它會繼承該使用者的環境與檔案權限。在筆記型電腦上,這是工作流程問題。在 agent 於 VPS 上無人值守執行時,這則是包含 4 個實務面向的安全問題。

Repository 中的 hook 是你未撰寫的程式碼。 .claude/settings.json 會被提交,因此複製 repository 並在其中啟動工作階段,可能會註冊 repository 附帶的 hook。Claude Code 會透過該資料夾的工作區信任對話框,控管 project hook。這表示接受信任的當下,就是你決定執行這些 hook 的時刻。請先閱讀 hooks 區塊。

Hook 可看見完整的 tool input。 會記錄 tool_input 的 audit hook,會將每個 command 的所有引數寫入檔案,包括碰巧出現在 command line 上的任何 token。該 log 之後需要與 secret 受到同等保護,這也是 讓 secret 遠離 AI agent 可存取範圍 這個更大問題的一部分。

Hook 可將內容寫入 model 的 context。 SessionStartUserPromptSubmit hook 輸出至 stdout 的任何內容,都會加入對話。將外部來源、issue tracker 或 log file 中的文字導入的 hook,會把不受信任的文字交給 model,效果就像是你親自輸入一樣。請將這些 stdout 視為 input,而不是 output。

權限才是真正的控制措施。 請讓 agent 以專用的非特權使用者執行,並只授予它所需的 sudo 規則。設定 PreToolUse deny 是有價值的;但這項機制本來就是 best effort:參考文件對 if filter 也有相同說明,並要求在需要強制 deny 時使用 permission system。真正能在壓力下維持作用的是 permission rules,以及執行該 process 的使用者帳號。

有一項特性在所有設定中都成立。PreToolUse hook 會在所有 permission mode 中,於 permission-mode check 之前觸發。因此,回傳 deny 的 hook 即使在 bypassPermissions 下,也會阻擋 tool。Hook 可以收緊 permission rules 所允許的範圍,但無法放寬該範圍。

我的 hook 為何沒有觸發?

請依照以下順序處理。每個步驟都說明你實際會看到的症狀。

  • 執行 /hooks,確認 hook 是否出現在預期的事件下。若 hook 未出現在選單中,通常表示設定檔有 JSON 語法錯誤,因為不允許使用尾端逗號和註解;也可能表示檔案不在上述 6 個位置之一。
  • 精確比對 matcher 與工具名稱。Matcher 區分大小寫,因此 "bash" 永遠不會比對到 Bash 工具。
  • 按照上方範例 1 的方式,手動搭配範例輸入執行指令碼。非預期的結束代碼表示指令碼本身有錯誤;Claude Code 會將其回報為 hook 錯誤,而不是決策結果。
  • 若看到 jq: command not found 的通知,表示該機器缺少 jq。若自己的指令碼出現 command not found,表示路徑解析失敗,請使用 ${CLAUDE_PROJECT_DIR} 或絕對路徑。若指令碼完全沒有執行,可能是因為沒有執行權限。
  • Hook 印出有效 JSON,但沒有任何作用。Shell 形式的 hook 會透過 sh -c 執行;若 shell profile 印出橫幅文字,該文字會加在 JSON 前面。此時 stdout 不再以 { 開頭,因此 Claude Code 會將整段內容視為純文字,並忽略該決策。結束代碼為 0 時,除了 debug log 之外,不會在任何位置回報內容。請將 profile 中的任何 echo 包在條件中,使其只在互動式 shell 執行。
  • 仍無法排除問題時,使用 claude --debug-file /tmp/claude.log 啟動工作階段,並在第二個終端機執行 tail -f /tmp/claude.log。Debug log 會記錄哪些 hook 符合條件、各 hook 回傳的結束代碼,以及它們寫入 stdout 和 stderr 的所有內容。

FAQ

Claude Code hook 與 CLAUDE.md instruction 有何不同?

CLAUDE.md instruction 是模型 context 中的文字,因此會與對話及目前的請求競爭注意力,模型可以權衡這些內容。hook 則是 Claude Code 在生命週期中的固定時間點執行的 shell command,因此每次事件發生時都會執行,不受模型決定影響。偏好設定應使用 instruction。必須一律執行的步驟,或絕對不可執行的動作,應使用 hook。

如何阻止 Claude Code 執行特定的 shell command?

註冊 PreToolUse hook,並設定 Bash matcher 從 .tool_input.command 讀取 command、將原因寫入 stderr,然後以 2 結束。Claude Code 會取消此次呼叫,並將原因顯示給模型。此動作發生在 permission-mode 檢查之前,因此即使在 bypassPermissions mode 中,拒絕仍然有效。針對 command string 的模式比對屬於防護措施,不是安全邊界,因為相同的 command 可能以模式無法比對的形式撰寫。因此,還應搭配 permission rules 及非特權帳號。

hook 輸出有效的 JSON,但沒有任何作用。為什麼?

最常見的原因是 shell profile。沒有 args field 的 hook 會透過 sh -c 執行,而某些 profile 會在每次 shell 啟動時輸出 banner,導致這段內容出現在 JSON 前的 stdout。由於輸出不再以 { 開頭,Claude Code 會將全部內容視為純文字並忽略該決策;此外,若以 0 結束,transcript 中完全不會顯示任何內容。請在 profile 中以 interactive-shell test 保護任何 echo,然後從 claude --debug-file /tmp/claude.log 讀取 debug log,以確認修正結果。

在共用伺服器上執行 Claude Code hook 是否安全?

hook 會以啟動 Claude Code 的使用者身分執行,並具備該使用者的檔案權限,因此 hook 可以執行該帳號有權執行的所有操作。以下兩項做法可涵蓋大多數風險:使用具有限 sudo policy 的專用非特權帳號執行 agent;在接受 repository 的 workspace trust dialog 前,先閱讀其中的 hooks block,因為 project hook 會隨 .claude/settings.json 一併發佈。若不希望執行任何此類 hook,請在 settings file 中設定 "disableAllHooks": true