Apple 開發中的 Hooks:拯救專案的模式
指向iOS專案的Claude Code工作階段,其觸及範圍是一般Python或網頁專案所不及的。Agent可透過Bash工具執行xcodebuild與xcrun,能讀取與編輯.pbxproj檔案(預設為舊式ASCII屬性列表,經工具轉換後有時為XML或JSON格式,無論哪種格式損毀都同樣致命)。它因執行於開發者的機器上,便掌握了開發者的簽署身分。它能抹除模擬器,能以錯誤的scheme重建專案,能commit並push。協定本身並未對此設下任何關卡:開發者的檔案系統就是agent的檔案系統,而Claude Code的--dangerously-skip-permissions旗標距離全自動化只有一個按鍵之遙。
緩解之道並非「信任agent」,而是hooks:主機在生命週期邊界(PreToolUse、PostToolUse、UserPromptSubmit、SessionStart、Stop)執行的確定性shell腳本。1Hooks會在面對危險輸入時讓agent放慢腳步、驗證破壞性輸出,並以綠色建置作為完成的關卡。它們是每位執行agent的iOS開發者在agent執行任何動作之前都應設定的承重安全原語。
四種hook模式值得在iOS專案中佔有一席之地。它們屬於框架層級而非專案專屬;本系列的應用程式(Return、Get Bananas、Reps、Water、Ace Citizenship)都執行這些模式的變體。每種模式都對應一個真實的失敗模式、一個具體的腳本,以及限縮爆炸半徑的生命週期事件。
TL;DR
- iOS專案上四種重要的hook模式:
.pbxproj驗證(PostToolUse,將錯誤回饋給agent)、危險bash把關(PreToolUse,執行前阻擋)、綠色建置Stop關卡、模擬器狀態整理(Stop)。 - Hook的退出碼很重要,且每個事件的行為都不同。
exit 2在PreToolUse會阻擋所提議的動作(工具永不執行);在PostToolUse則無法阻擋(工具已執行),但會將stderr回饋給agent讓它修復或還原;在Stop則阻止agent結束工作階段。exit 0允許。exit 1通常會記錄但不阻擋。1 - Hook腳本位於儲存庫的
.claude/hooks/*.sh中,由.claude/settings.json以相對路徑參照。同樣需經過程式碼審查。 - Agent的權限就是開發者的權限。Hooks是開發者將該權限重新雕琢回一組刻意核可動作的方式。
模式一:每次編輯都驗證.pbxproj
Xcode專案檔是agent經常修改、且每行爆炸半徑比例最高的檔案。project.pbxproj中一個錯誤的括號就會悄悄破壞團隊每位開發者的建置。建置錯誤會在下次xcodebuild執行時出現,而非編輯當下,因此agent通常會在破壞浮現之前宣稱變更已成功。
此hook會對任何.pbxproj寫入執行plutil -lint。PostToolUse無法阻擋寫入本身(hook觸發時檔案已寫入磁碟),但exit 2會將驗證錯誤立即作為工具呼叫失敗回饋給agent:agent讀取失敗訊息、得知檔案已損毀,便能在工作階段繼續前還原或修復:
#!/bin/bash
# .claude/hooks/post-write-pbxproj.sh
# Runs after every Edit or Write tool call. Exits 2 to surface the
# validation failure to the agent so it can revert/repair the broken
# .pbxproj before the session moves on. (PostToolUse cannot prevent
# the write itself; it can only feed the error back.)
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ "$FILE" != *.pbxproj ]]; then
exit 0
fi
if ! plutil -lint "$FILE" >/dev/null 2>&1; then
echo "ERROR: $FILE failed plutil -lint after write" >&2
echo "The Xcode project file is structurally broken. Revert and try again." >&2
exit 2
fi
exit 0
plutil -lint會捕捉結構性損壞:括號或圓括號不對稱、缺少分號、無效的plist token、XML巢狀結構損毀。2但它無法捕捉Xcode語意錯誤,例如剛好是語法上有效plist文字的格式錯誤UUID,或參照了不存在檔案的build phase。這些會產生agent能正常除錯的一般建置錯誤。plutil關卡負責捕捉災難性解析失敗類別;語意錯誤則交由建置本身處理。
.claude/settings.json中的hook設定(請注意路徑含空格時要將$CLAUDE_PROJECT_DIR加上引號):
{
"hooks": {
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-write-pbxproj.sh"
}]
}]
}
}
matcher只在Write和Edit工具呼叫上觸發hook;腳本的第一個動作就是對非.pbxproj路徑短路。在每次Edit上執行的成本可忽略不計,因為路徑過濾是首要檢查。
模式二:在執行前把關破壞性Bash命令
xcrun simctl erase會抹除模擬器資料。xcodebuild archive會啟動簽署流程,可能產生開發者並未打算簽署的成品。git push --force會改寫歷史。Agent透過Bash工具能存取這一切。Bash上的PreToolUsehook會比對所提議的命令模式,並決定是否放行。
形式如下:
#!/bin/bash
# .claude/hooks/pre-bash-xcode.sh
# Runs before every Bash tool call. Exits 2 (blocking) on irreversible
# Xcode/signing/git operations the developer hasn't explicitly approved.
# PreToolUse hooks CAN block: exit 2 prevents the tool from running.
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
case "$COMMAND" in
*"simctl erase"*)
echo "ERROR: simulator erase requires explicit human approval" >&2
echo "Tell the developer what you wanted to erase and why; let them run it." >&2
exit 2
;;
*"xcodebuild archive"*|*"xcodebuild -exportArchive"*)
echo "ERROR: xcodebuild archive/export invokes signing; requires human approval" >&2
exit 2
;;
*"git push --force"*|*"git push -f"*)
echo "ERROR: force-push rewrites history; requires human approval" >&2
exit 2
;;
*"rm -rf"*)
echo "ERROR: rm -rf requires explicit approval" >&2
exit 2
;;
esac
exit 0
Hook即是一個基於命令模式子字串的switch。阻擋的類別是不可逆動作類別:抹除、簽署、強制推送、遞迴刪除。可逆操作(一般建置、測試、未加–force的git commit)則放行。
常見的精煉做法是允許開發者在對話中透過環境變數或旗標明確選擇加入時放行某些命令。例如:若CLAUDE_ALLOW_ARCHIVE=1存在於環境中(由開發者在工作階段前為特定archive任務設定),則允許xcodebuild archive。Hook讀取環境變數並繞過阻擋:
*"xcodebuild archive"*)
if [[ "${CLAUDE_ALLOW_ARCHIVE:-0}" == "1" ]]; then
exit 0
fi
echo "ERROR: xcodebuild archive requires CLAUDE_ALLOW_ARCHIVE=1" >&2
exit 2
;;
這個模式:對不可逆類別預設拒絕,為開發者希望agent處理的情況提供opt-in的逃生閥。
模式三:以綠色建置作為完成關卡的Stop hook
當對話看似已解決時,agent傾向宣告任務完成。若無關卡,「完成」可能意味著「我編輯了檔案而聊天處於連貫狀態」,而非「建置仍能編譯」。Stophook正是強制執行正確意義的所在。
#!/bin/bash
# .claude/hooks/stop-build-check.sh
# Runs when the agent tries to stop. Exits 2 if the build is broken,
# which prevents the session from concluding until the agent fixes it.
cd "$CLAUDE_PROJECT_DIR" || exit 0
# Hard-code project, scheme, and destination per repo. Do not rely on
# auto-discovery: workspaces, multiple projects, or shared-vs-user
# schemes all break naive heuristics.
PROJECT="MyApp.xcodeproj"
SCHEME="MyApp"
DESTINATION="platform=iOS Simulator,name=iPhone 17 Pro"
LOG=/tmp/claude-stop-build.log
if ! xcodebuild -project "$PROJECT" -scheme "$SCHEME" \
-configuration Debug \
-destination "$DESTINATION" \
build > "$LOG" 2>&1; then
echo "ERROR: build failed; cannot stop with a broken build" >&2
echo "See $LOG for the full xcodebuild output." >&2
tail -50 "$LOG" >&2
exit 2
fi
exit 0
對隨儲存庫提交的hooks而言,硬編碼PROJECT、SCHEME與DESTINATION是正確的形式:值不會漂移、workspaces與多專案儲存庫無需逐機調整即可運作,使用相同hook的CI建置系統也能透過環境變數替換destination。自動發現(ls *.xcodeproj、xcodebuild -list | awk)對最簡單的單人案例可行,但會在以.xcworkspace為根的專案、含多個.xcodeproj檔的儲存庫,以及shared-vs-user scheme的區分上失敗。Destination字串遵循xcodebuild已記載的platform=...,name=...語法;3它必須是開發者機器實際擁有的模擬器,否則hook會因環境因素而非程式碼因素失敗。
Hook做出兩個產品決策:
Stop阻擋的是agent的「我完成了」訊號,而非人類的。開發者隨時可以Ctrl+C、關閉終端機或覆寫。Hook是針對agent樂觀主義的護欄,而非對人類的鎖定。
Hook執行的是真正的建置,而非語法檢查。對iOS專屬專案執行swift build會跳過iOS專屬的編譯步驟;唯有xcodebuild能證明iOS目標可編譯。代價是建置時間本身(多數專案為10至60秒);價值在於每次都捕捉到建置損壞卻被標示為完成的情況。
模式四:模擬器狀態整理
長時間agent工作階段後,模擬器可能堆積:agent忘記關閉的已啟動模擬器、快取陳舊狀態的舊應用程式安裝、跨工作階段存活並產生不可重現錯誤的執行階段資料。Stophook可以負責清理。
#!/bin/bash
# .claude/hooks/stop-simulator-cleanup.sh
# Soft cleanup: shuts down booted simulators we don't need anymore.
# Does NOT erase data; does NOT block. Logs only.
BOOTED=$(xcrun simctl list devices booted 2>/dev/null | grep -E "Booted" | wc -l | xargs)
if (( BOOTED > 0 )); then
echo "[hook] $BOOTED booted simulators at session end; consider shutdown" >&2
# Uncomment to auto-shutdown:
# xcrun simctl shutdown all 2>/dev/null
fi
exit 0
形式刻意設計為非阻擋:hook回報狀態但除非開發者取消關機行的註解,否則不會行動。原因在於agent的下一個工作階段可能希望保留已啟動的模擬器(模擬器已在執行時冷啟動較快)。決策因開發者而異:若模擬器在多個工作階段間堆積且代價真實,則取消關機的註解;否則保留為日誌訊號即可。
更激進的變體會在工作階段間抹除模擬器,但這已跨入模式二的破壞性操作範疇。Erase屬於PreToolUse阻擋的範疇,而非Stop自動化。
Hooks無法解決的問題
上述四種模式是工作集,並非全貌。三類hooks無法捕捉的失敗:
Agent程式碼中的邏輯錯誤。Hook驗證結構而非語意。Agent可以撰寫一個能編譯、通過專案檔lint、建置綠燈,但語意上仍錯誤的@Model類別(缺少migration、唯一性約束損壞、SwiftData關聯沒有逆向關係)。邏輯正確性存在於測試、程式碼審查與開發者的眼睛之中;hooks是處理結構性與生命週期關注點的。
Agent品質的緩慢漂移。每個個別hook能在首次遭遇時阻擋一類失敗,但跨多個工作階段的累積漂移(程式碼逐漸雜亂、測試逐漸薄弱、CLAUDE.md指令逐漸過時)並非hooks能衡量的。那是工作階段審查問題,而非hook問題。
Agent工具表面之外的信任邊界違規。Bash與Edit上的hook涵蓋了常見路徑。對agent可能呼叫的每個MCP工具設定hook需要逐工具的matcher;某些MCP伺服器公開數十甚至上百種工具(XcodeBuildMCP約有80個),逐工具撰寫hook並不切實際。正確的模式是限縮MCP伺服器存取(專案層級的.mcp.json、首次使用時的核可流程),而非為每個個別工具設定hook,並接受agent運作其MCP伺服器是其受核可權限的一部分。
Hooks與更廣泛信任態勢的關係,在The Repo Shouldn’t Get to Vote on Its Own Trust中有所討論:信任是載入順序的不變量,而非下游檢查。Hooks是對已受信任agent所採取行動的下游守衛;它們無法取代上游關於agent是否該被信任的決策。
我會以不同方式建置的部分
本系列應用程式已實作或希望已實作的三種模式。
將Hook腳本與專案其餘部分一同納入版本控制。Hook腳本位於儲存庫的.claude/hooks/*.sh。.claude/settings.json以相對路徑參照它們。團隊跨機器獲得相同的安全網、hook變更納入程式碼審查,新開發者上手只需git clone而非複製貼上。位於~/.claude/settings.json的使用者範圍hooks對於專案專屬把關而言粒度錯誤。
列印作用中hook設定的SessionStart hook。Hooks在觸發前是沉默的。一個在每次Claude Code工作階段開始時執行、列印「Active hooks: pbxproj-validation, dangerous-bash-gate, build-check-on-stop」的SessionStarthook,會提醒開發者(與agent)有哪些守衛正在執行。代價是每次工作階段一行stderr;價值在於沒有人會在不知道安全網存在的情況下進行開發。
儲存庫層級的agent工具呼叫稽核日誌。一個PostToolUsehook將每次工具呼叫(含時間戳、工具名稱、引數)附加到.claude/logs/中的JSONL檔案(已gitignore)。日誌以jq查詢回答「agent本次工作階段做了什麼?」,而非滾動聊天歷史。Hook每次工具呼叫增加數毫秒,並產生開發者在出問題時可grep的耐久稽核資料。
何時Hooks是錯誤答案
兩種hook層是解決問題錯誤位置的情況。
Agent的MCP伺服器本身。有問題的MCP伺服器做出錯誤行為並非hook問題;那是MCP伺服器問題。修正之道是限縮專案信任的MCP伺服器(.mcp.json審查、專案範圍的首次使用核可),並在伺服器為開源時閱讀其原始碼。對每個MCP工具呼叫設定hook只會增加開銷,無法解決信任問題。
無人值守執行的Agent。完整的hook態勢假設開發者在工作階段附近,能解讀失敗的hook。在CI中無人介入執行的agent需要不同的態勢:更嚴格的MCP限縮、更窄的工具集、不同的信任模型。Hooks單獨無法在有人值守的開發與無人值守的自動化之間搭橋;那道鴻溝是刻意的。
此模式對在iOS 26+上推出iOS應用程式的意義
三點啟示。
-
對不可逆操作預設拒絕、驗證結構性爆炸半徑檔案、以綠色建置作為完成關卡。三個hook生命週期事件(
PreToolUse、PostToolUse、Stop)、四種模式,涵蓋常見的iOS失敗模式。整體組合小到一個下午就能寫完,又耐久到能比任何特定agent或模型活得更久。 -
退出碼很重要,且因事件而異。
exit 2在PreToolUse會阻擋動作(工具永不執行);在PostToolUse無法阻擋(工具已執行),但會將stderr回饋給agent讓agent修復或還原;在Stop會阻止agent結束工作階段。exit 1在多數事件中不會阻擋。在依賴每個hook之前,先以刻意失敗的案例測試它。 -
Hooks限縮權限,並不授予權限。Agent對開發者機器的觸及範圍,就是OS允許開發者終端機工作階段所能做的一切。Hooks讓開發者從該權限中雕琢出特定動作並要求明確核可。預設值是OS所授予的;hooks的目標是讓預設值更小,而非更大。
完整的Apple Ecosystem系列:為Apple Intelligence表面提供的型別化App Intents;為agent表面提供的MCP伺服器;兩者間的路由問題;為應用程式內裝置端LLM功能提供的Foundation Models;執行階段與工具LLM的區別;三個表面的綜合;單一真相來源模式;與這些hooks搭配的Xcode整合兩個MCP伺服器;iOS鎖定畫面狀態機的Live Activities;Apple Watch上的watchOS執行階段契約;框架基底的SwiftUI內部結構;visionOS場景的RealityKit空間心智模型;持久化的SwiftData schema紀律;視覺層的Liquid Glass模式;跨裝置觸及的多平台出貨。集散中心位於Apple Ecosystem Series。如需更廣泛的iOS搭配AI agents內容,請參閱iOS Agent Development指南。
FAQ
什麼是Claude Codehooks,為何對iOS開發如此重要?
Claude Codehooks是在生命週期事件(PreToolUse、PostToolUse、UserPromptSubmit、SessionStart、Stop)執行的確定性shell腳本。對iOS開發而言,它們限縮agent對破壞性操作的權限:模擬器抹除、程式碼簽署、專案檔案修改、強制推送。沒有hooks時,agent擁有開發者機器的完整權限;有了hooks,特定危險動作便需要明確核可。
iOS開發者應優先處理哪些hook事件?
Bash上的PreToolUse用於阻擋破壞性命令(simctl erase、xcodebuild archive、git push --force)。Edit/Write上的PostToolUse用於驗證.pbxproj完整性。Stop用於以綠色建置把關。SessionStart用於記錄作用中的hook設定。這四者合併捕捉最常見的iOS專屬agent失敗。
退出碼0、1與2的差別是什麼?
Exit 0允許動作並繼續。Exit 2因事件而異:在PreToolUse會阻擋所提議的動作(工具永不執行);在PostToolUse因工具已執行而無法阻擋,但會將stderr回饋給agent讓agent修復或還原;在Stop會阻止agent結束工作階段。Exit 1會記錄錯誤但在多數hook事件中不會阻擋。對於需要在動作執行前實際阻擋的安全模式,請在PreToolUse使用exit 2。對於破壞性寫入後的驗證,請在PostToolUse使用exit 2將失敗回饋給agent。在每個hook上以刻意失敗的輸入測試,以確認其在特定事件中行為符合預期。
Hook腳本應放在哪裡?
放在專案根目錄的.claude/hooks/*.sh,由.claude/settings.json以相對路徑參照。與專案其餘部分一同納入版本控制與程式碼審查。位於~/.claude/settings.json的使用者範圍hooks也能運作,但對於專案專屬的iOS把關而言是錯誤的粒度。
Hooks能取代程式碼審查的需求嗎?
不能。Hooks在問題出貨前捕捉結構性錯誤(損壞的專案檔、危險的bash、損壞的建置)。程式碼審查捕捉語意錯誤(邏輯錯誤、缺少migration、薄弱的測試)。兩個層次互補:hooks讓agent在內部循環中部署起來更安全,程式碼審查在邊界上保持agent輸出的誠實。
參考資料
-
Anthropic,“Claude Code reference: Hooks”。生命週期事件(
PreToolUse、PostToolUse、UserPromptSubmit、SessionStart、Stop)、matcher語法、命令形式以及退出碼的角色。Exit 2因事件而異:在PreToolUse與Stop會阻擋動作;在PostToolUse無法阻擋(工具已執行),但會將stderr回饋給agent。Exit 0允許;exit 1通常記錄但不阻擋。作者於When the LLM Lives in Your App vs in Your Tooling中的分析涵蓋了hooks所操作化的執行階段對工具LLM信任態勢。 ↩↩ -
Apple,
plutil(1)man page。-lint旗標跨舊式ASCII、XML與二進位格式驗證屬性列表語法。它能偵測解析層級的損壞,但不檢查Xcode專屬語意,例如build phase參照或專案圖中的UUID有效性。 ↩ -
Apple Developer,“xcodebuild Destination Specifier”與
xcodebuildman page。-destination 'platform=...,name=...'語法是釘選建置目標的標準方式;CI環境透過環境變數或腳本化裝置可用性偵測來覆寫模擬器名稱。 ↩