← 文章

那天 AI 決定「清理」,專案沒了

· 15 分鐘閱讀 · 418 次閱讀
AI 輔助開發 Hooks 護欄 Context 管理 可觀測性 開源
AI 工程實戰

那天 AI 決定「清理」

專案沒了

同事的 AI CLI 工具刪了整個專案資料夾,差點無法復原。現在每個人都開全權限跑 AI——沒有護欄的全自動,就是全自動的災難。

事件

那天 AI 刪了資料夾

真實事件,兩位同事,兩個不同的 CLI 工具

兩位同事各自被 AI 刪過東西:一位用 OpenAI Codex CLI,一位用 Google Gemini CLI,兩個工具都很強大,事後才發現都沒設定任何 hook。

第一位同事讓 AI 清理暫存檔,AI 判斷出偏差,直接刪掉整個專案資料夾。

$ rm -rf ./project-name
# AI 認為這是「清理」的一部分
# 沒有任何機制攔截這個指令

靠 git history 和 Time Machine 備份,花了不少時間才復原。第二位同事也出了事:AI 整理檔案時誤判路徑,刪掉了不該刪的目錄。

兩次事件的共同點:CLI 工具本身沒有問題,問題是沒有護欄。

AI 是機率性的系統。即使 prompt 寫了「不要刪除重要檔案」,模型仍然可能在某次推理中做出不同的判斷。Hooks 是確定性的——rm -rf / 永遠會被擋下,不管 AI 怎麼想。

現實

沒有人在一個一個 Allow

實際上沒有人一次次按 allow——AI CLI 工具理論上都有權限確認機制,每次執行指令前會問 allow 或 deny,但一天要按幾百次,開發效率直接歸零,所以每個人都開最高權限:

# Claude Code — 跳過所有權限確認
claude --dangerously-skip-permissions

# OpenAI Codex CLI — YOLO 模式,自動批准一切
codex --approval-mode full-auto

# Google Gemini CLI — 同樣的 YOLO 模式
gemini --sandbox=false

這不是少數人的行為,這是常態:開全權限是唯一能讓 AI agent 真正發揮效率的方式,但代價是 AI 可以執行任何指令——刪檔案、force push、寫入系統目錄、推送含密鑰的 commit。沒有護欄的全自動,等於把開發環境交給一個「大部分時候很聰明,偶爾會犯錯」的系統。

問題不是「要不要開全權限」——而是「開了之後,誰來擋住那個偶爾?」

問題

散落的 Shell Scripts 會失控

接著我們開始幫 Claude Code 加 hooks,一開始只是寫幾個 shell script 擋危險指令,三個月後就自己先出包:腳本散落、一次 crash 全部停擺、沒有開關可以關。

1

腳本散落各處

十幾個 .sh 檔案,有的在 hooks 目錄,有的在 scripts 目錄,有的在 home 目錄。每個都重複實作 JSON 解析和錯誤處理。

2

一個 crash 全部停擺

某個 hook 因為 JSON 格式異常而 crash。Claude Code 的 hooks 機制是同步的:一個 hook 崩潰,整條 pipeline 中斷。

3

沒有 on/off 開關

想暫時關掉某個 hook?要到 settings.json 裡手動改。改完測試後,經常忘記改回去。

解法

一個 Dispatcher,管理所有 Hooks

v0.1.0 這一版,我們把所有 hooks 整合成一個模組化框架:一個 dispatcher 接收所有事件,依設定檔分派到各個 handler。

34
Handler 模組
10
事件類型全覆蓋
1
YAML 設定檔
0
外部依賴
settings.json
Claude Code 原生設定。10 個事件類型,每個指向同一個 dispatcher
Claude Code
dispatcher.py
薄 shim,讀事件類型和 JSON 輸入,呼叫 handler registry
進入點
Two-Phase
Critical handlers 先跑(無預算限制),Deferrable handlers 在 5 秒預算內依序執行
調度策略
config.yaml
每個 handler 獨立開關。一行 YAML 切換,不需要碰 settings.json
設定層

Two-Phase Dispatch 是關鍵設計。安全類的 handler(如擋 rm -rf)標記為 critical,永遠第一個跑、不受時間預算限制。其他 handler 共用一個 5 秒預算——超時就跳過,但不會阻塞 AI 的工作流。

安全護欄

擋住 AI 的危險操作

第一位同事誤刪整個專案資料夾那種事故,擋得住的是這三個 handler——它們標記為 critical,無條件優先執行,處理最關鍵的安全問題。

bash_safety
PreToolUse · Bash

擋 rm -rf /、sudo、git push --force、chmod 777 等危險指令。用 quote-aware 的 shell parser 解析命令,不會被引號或子 shell 繞過。

secret_scan
PreToolUse · Bash

在 git push 前掃描 diff,攔截 AWS Key、GitHub Token、PEM 私鑰等。只在推送時觸發,不干擾本地開發。支援 # nosec 逐行豁免。

skill_security
PreToolUse · Write/Edit

擋 prompt injection 攻擊。偵測「ignore previous instructions」等注入語句、dangerouslyDisableSandbox 等提權操作、curl 外傳資料等竊取行為。三類威脅,獨立掃描。

「當時如果有 bash_safety,第一位同事的資料夾就不會被刪。這是我們第一個寫的 handler,也是整個專案的起點。」

品質閘門

讓 AI 自己檢查自己的作業

commit 之前一定要把驗證跑完,這是品質閘門那四個 handler 立下的規矩。在那之前,驗證跑不跑全看 AI 自己——它寫完程式碼常直接 commit、跳過驗證。

verify_commit
PreToolUse · Bash

攔截 git commit 和 gh pr create。要求 AI 先跑完驗證(lint、typecheck),通過後寫入一個 marker 檔案,才放行 commit。一次性令牌:用完即消。

verify_completion
SubagentStop

Sub-agent 完成任務時,自動跑 lint 和 typecheck。如果不通過,把 agent 打回去修。最多 5 次迴圈、30 分鐘上限——超過就放行,避免卡死。

auto_format
PostToolUse · Edit/Write

AI 編輯完檔案後,自動跑 ruff(Python)或 biome(JS/TS)格式化。永遠不會阻擋——即使 formatter crash 也靜默放行。格式統一是好事,但不值得中斷工作流。

review_gate
Stop

Claude Code session 結束時,檢查是否有未 commit 的程式碼變更。預設只發出提醒(不阻擋),需要手動 opt-in 才會真正 block。避免 AI 做完事就走,改動留在 working tree 裡。

Context 管理

讓 AI 不要失憶

一個 sub-agent 生出來就是空白的,主對話的脈絡不會自動跟過去。Context 管理那三個 handler 補的正是這一段——AI Agent 的 context window 本身也有限,記不好等於每次都重新來過。

context_inject
SubagentStart

Sub-agent 啟動時,自動注入專案文件。讀取 .context/ 目錄下的 JSONL 設定檔,每個 agent type 可以有不同的注入內容。上限 8000 字元,避免塞爆 context。

context_relay
SessionStart · PreCompact

Session 之間的記憶接力。上一個 session 結束前寫好的交接文件,在新 session 啟動時自動注入。5 分鐘 TTL,防止過期資料干擾。

plan_impl_gate
PostToolUse · ExitPlanMode

AI 退出 Plan Mode 時,提醒先把計畫存到記憶裡再開始實作。因為 Plan Mode 本身消耗大量 context,不存就浪費了。一次性提醒,不重複。

治理與觀測

看見 AI 在做什麼

sub-agent 一多,背景在忙什麼就看不到——這裡靠四個 handler 找回可見度。

agent_naming
PreToolUse · Agent

強制每個 sub-agent 有描述性名稱。沒取名就擋下來。同時根據 prompt 內容建議合適的 agent type——寫程式碼用 worker,查資料用 explorer,寫文件用 writer。

observability
所有事件

每個 hook 事件都寫一行 JSONL 到 spool 檔案。延遲 <1ms,不影響任何操作。背景程式每 2 秒批量寫入資料庫,供 dashboard 查詢。整個鏈路零網路延遲。

sentinel_notify
PreToolUse · PostToolUse · Bash

AI 執行 service restart 或 docker 操作時,通知監控系統進入維護窗口。操作結束後自動解除。讓監控系統知道「這段停機是預期的」,不會誤報警。

voice_notify
Stop · AskUserQuestion

AI 做完工作時,用 TTS 語音通知你。不用盯著螢幕等——去泡咖啡,完成了它會叫你。需要你回答問題時也會用語音提示。七層 guard 機制過濾 sub-agent 的中間狀態,只在真正完成時才發聲。

設計哲學

三個原則,貫穿所有 Handler

Block-at-Submit,不是 Block-at-Write

AI 寫檔案時不擋。在它要 commit 或 push 時才擋。

原因:AI 的工作流是連貫的。如果在 Edit 時就 block,AI 會失去上下文,不知道自己寫到哪。等到它要提交成果時再審查,更合理。

Fail-Open,永遠

每個 handler 都包在 try/except 裡。Handler crash 了?放行。Config 讀取失敗?放行。

原因:護欄不應該比它保護的東西更危險。一個 crash 的 handler 不該讓整個 AI 工作流停擺。

每條規則都有逃生口

secret_scan 有 # nosec,verify_commit 有 marker 機制,bash_safety 有環境變數開關。

原因:沒有逃生口的規則,最終會被整個關掉。給使用者繞過的方式,反而能讓規則活更久。

刻意不做

比做了什麼更重要的,是沒做什麼

  • 不攔截 Edit / Write AI 寫檔案時不干預。中途打斷 AI 的寫入流程,會造成不完整的檔案狀態。所有審查都延遲到 commit 或 push 時進行。
  • 不在閘門裡自動跑測試 verify_completion 只跑 lint 和 typecheck,刻意排除測試。測試太慢、有副作用、可能改變資料庫狀態。放在自動閘門裡,風險比收益大。
  • 不自動修復,只阻擋並解釋 bash_safety 不會幫 AI 改寫「更安全的指令」。它只說「這個指令被擋了,因為 X」。讓 AI 自己決定下一步。自動改寫容易引入更詭異的 bug。
  • 不做 context budget 評分 我們寫過一個 context_supervisor handler(800 行),嘗試計算 context 使用量並建議壓縮時機。概念很好,但評分模型不準確——context 的「重要性」無法用啟發式規則判斷。已禁用。
  • 不 Fail-Closed 所有 handler crash 都放行。有人會問「這不危險嗎?」——更危險的是護欄自己把整個系統卡住。AI 不動了,使用者也不知道為什麼。
開源專案

hook-observatory

MIT 授權,零外部依賴,裝完即用。
三種安裝方式:Homebrew、Git Clone、或桌面安裝器。

★ GitHub ↓ 下載 Installer

Homebrew

macOS 推薦。兩行搞定,不需要 clone。

brew tap operonlab/tap
brew install hook-observatory

# 安裝到 Claude Code
hook-observatory
Git Clone

所有平台通用。直接跑安裝腳本。

git clone https://github.com/operonlab/hook-observatory.git
cd hook-observatory
python3 install.py
安裝器

GUI 引導式安裝,勾選 handler、設定工具路徑。

macOS 首次開啟需執行:xattr -cr 安裝器.app

.dmg Apple Silicon .dmg Intel Mac .exe Windows

工具

貼幾行指令,看哪幾行會被擋

bash_safety 那張卡片點名了四類指令:rm -rf、sudo、git push --force、chmod 777。貼幾行你自己跑過的指令,這裡照那四類逐行比對,順便標出哪幾行會撞到 verify_commit 或 secret_scan。全部在你的瀏覽器裡跑,沒有任何東西被送出去,也沒有任何東西被執行。貼幾行就好,不用整份 history——含密鑰、內部主機名、私有路徑的那幾行留著別貼。

常數出處都在本文:四類危險指令與「用 quote-aware 的 shell parser 解析命令,不會被引號或子 shell 繞過」出自 bash_safety 卡片;git commit / gh pr create 要先跑完 lint、typecheck 才放行,出自 verify_commit 卡片;git push 前掃 diff 出自 secret_scan 卡片。這裡做不到的四件事:① 判的是你貼進來的那串字,不是你機器上真的會發生什麼——沒有任何東西被送出去,也沒有任何東西被執行 ② 這是照本文點名的四類做的近似判斷,不是 hook-observatory 的實際規則;本文寫「等危險指令」,代表那份清單本來就不完整,所以「沒被標紅」不等於安全 ③ 本文第三個 critical handler(skill_security)掛在 PreToolUse · Write/Edit,看的是檔案內容不是指令列,這裡查不到 ④ 照本文〈刻意不做〉那條,被擋只會告訴你原因,不會給你一個「比較安全的寫法」。

帶走這段

複製給你的 AI Agent

問題不是「要不要開全權限」,是「開了之後誰擋住那個偶爾」——prompt 寫「別刪重要檔案」沒用,AI 是機率性的,某次推理就翻臉;hook 是確定性的,rm -rf / 永遠擋下。這篇的起點就是第一位同事被 AI 刪掉整個專案資料夾。

把下面貼給你的 AI,讓它先查你是不是在裸奔,再照本文的護欄清單補:

我在看一篇文章〈那天 AI 決定「清理」,專案沒了〉 (https://blog.joneshong.com/zh/blog/hook-observatory), 作者的同事被 AI 一句 rm -rf 刪掉整個專案,於是做了一套 hook 護欄: 一個 dispatcher + 分類 handler,安全類標記 critical 永遠先跑(Two-Phase)。 請先查我現在的實際狀態,再照這套幫我補,別給通用清單: 先查我是不是在裸奔: 1. 我用哪個 CLI?是不是開了全權限跑 (claude --dangerously-skip-permissions / codex full-auto / gemini --sandbox=false 這類) 2. 我現在有沒有任何 hook / pre-commit?翻我的 settings.json、hooks 目錄、.git/hooks 看有沒有東西 3. 我最近的 shell history 有沒有出現過危險指令卻沒被擋 再照本文的 handler 分類,判斷我最該先補哪幾個: - bash_safety:擋 rm -rf /、sudo、git push --force、chmod 777(要 quote-aware,別被引號或子 shell 繞過) - secret_scan:push 前掃 diff 裡的 AWS Key / GitHub Token / PEM 私鑰 - skill_security:擋 prompt injection、dangerouslyDisableSandbox、curl 外傳 - verify_commit:commit 前逼過 lint / typecheck 才放行 回我:照「一旦出事有多痛」排序,先寫哪 3 個 handler,各自的逃生口(例如逐行豁免)怎麼設計。 如果我根本沒開全權限、每條指令都自己按 allow,就直說我可能還不需要這套。
參考資料

延伸閱讀

Blake Crosley 那篇〈Why Each of My 95 Hooks Exists〉點醒了我:每個 handler 該對應一個真實事故。

資源 為什麼重要
operonlab/hook-observatory 本文討論的開源專案
Blake Crosley: Why Each of My 95 Hooks Exists incident-driven 的 hook 建構思路。每個 hook 背後都有一個事故。
GitButler: Automate Your AI Workflows with Claude Code Hooks 最早的 hooks 實戰分享之一,啟發了我們的初始設計
aiorg.dev: Claude Code Hooks Complete Guide 「確定性 vs 機率性」的框架描述,精準表達了 hooks 的價值定位
Jackle: Claude Code Voice Mode 用 hooks 實現語音通知的實作參考,啟發了 voice_notify handler 的設計
✦ 帶走這段