我有一份內建拒絕清單,它確實擋下了該擋的那一類指令。但我在寫自己的安全說明文件時才意識到:同一件事換個外層寫法再送一次,它就會過——因為清單比對的是指令長什麼樣子,不是這道指令會做什麼。
Day 7 的結論是:證據管事後可查,管不了事前該不該。用驗收制度去補權限是補不起來的,等你拿得出證據,事情已經做完了。刪掉的目錄不會因為 diff 乾淨、測試全綠就回來。所以這一層必須提前到動作發生之前。
內建拒絕清單是工具本身就有的機制,我一開始以為這樣就夠了。
問題是它只看送進來的那一行字面。同一個行為可以有很多種寫法:包裝一層、換個等價寫法、命令替換——清單對不上,就放行了。這件事寫在我自己的安全說明文件裡,把這一層明白降級成「粗篩」,並註明指令經過包裝即可繞過,故只算第一層(這是文件上的設計判斷,我沒有另外做繞過實測,也不會在這裡寫任何具體寫法)。
粗篩不是沒用,它成本很低,但它只擋字面;它擋不住的那一類,恰好就是「有人(或有 agent)稍微想一下就能寫出來」的那一類。而我的執行者——被派出去做事的 subagent——整天都在組指令。它不需要惡意,只要在某一次組出一個等價寫法,這層就形同不存在。
所以第二層出現了:一個掛在「工具呼叫送出之前」的攔截機制。它不在指令執行完之後記帳,而是在指令要被執行的那一刻先被問過。

第一層只算粗篩,真正在動作發生前擋下來的是第二層;而第二層看得到的,也只有經由工具呼叫送出的指令。
這層的判定分三檔:拒絕、詢問、放行。幾個設計細節值得單獨標出來(以下機制描述出自我的安全說明文件,屬文件宣稱):
唯讀工具豁免。 只讀不寫的工具不進判定直接放行。我的理解是,這不是偷懶,而是讓成本花在會改變狀態的動作上——不豁免的話,每次讀檔都要走一遍判定,這層會先被自己的開銷拖垮。這個豁免發生在粗篩之前:先判是不是唯讀工具,不是才往下走三檔判定。
命令替換要遞迴檢查。 一道指令裡如果還嵌著另一道指令的結果,外層看起來乾淨不代表內層乾淨,所以內層要被拆開來再判一次。這是「只看字面就會漏」的直接補救。
攔截機制會保護自己。 動到攔截機制自身檔案的寫入、刪除、搬移、改權限,一律進詢問檔。理由很白:一個可以被它所管轄的對象隨手改掉的規則,不是規則。
機制本身有回歸測試釘著。我實測跑過一次,823 個測試案例全數通過,結尾印出全過;測試腳本自己開一個獨立的暫存紀錄目錄、結束時清掉,不會寫進真實的攔截紀錄,所以下面的統計不會被測試污染。
紀錄這邊,現存檔案共 5534 行,時間範圍 2026-09-03 到 2026-09-20。其中要扣掉 2 行:那是某次測試指令的內文裡帶著換行,被逐行寫入時誤收成獨立記錄的假資料,已從所有統計排除(2 行 / 5534 行,約 0.036%)。這份紀錄在我本機,未公開,讀者無法重跑,下面的數字只能當成一個樣本來看。

同一批紀錄的兩種口徑。左邊是機器判了幾次,右邊是人真的按了幾次;兩張圖的數字不可相減。
我另外對這批紀錄做過一次粗略分類,最大宗是「其他未分類」(629 筆詢問)。分類只是事後粗略歸因,同一道指令可能同時像好幾類,所以只能看輪廓,不能當成精確統計。
**口徑一,機器判定檔位:**詢問 1150 次、拒絕 358 次。這要分拆成兩組:真的攔下來的是詢問 728 次、拒絕 340 次;2026-09-16 開始的影子模式——判定照跑、照記錄,但不真的攔下來——是詢問 422 次、拒絕 18 次。
**口徑二,人工回應:**放行 404 次、拒絕 24 次。這只在真的跳出詢問、而且有人回答時才會留下一列;這 428 次回應對應的是真的跳出詢問的那一組(詢問 728 次)。
這兩組數字不能相減,也不能拿來算比例。詢問發生了但沒有人回應——對話結束、沒有終端機可以按、任務被取消——都不會留下任何一列人工回應。差額多半是沒有人回應,另有少數是被自動降級處理掉的——後者可以查得到的只有 1 筆。任何把它解釋成「使用者沒理它」的說法都是編的。這是我自己踩過的坑:兩個數字放在同一張表上,很容易就順手減了。
三檔裡最脆弱的是中間那檔。詢問這個設計預設了一件事:現場有人可以回答。
我的多數工作跑在背景,沒有人盯著。那種情境下,跳出詢問的實際效果不是「等待決策」,是靜默停等:任務停在那裡,不報錯、不結束、也不會有人知道。
自製記憶系統(我自己的決策紀錄)裡 2026-09-07 那筆決策原文就記著這個理由——背景任務沒有終端機,詢問必然停等,曾經卡了 39 分 32 秒。這筆是當時留下的文字紀錄,我沒有重跑驗證。
所以多了一個無人值守開關:打開之後,本來會跳詢問的一律判成拒絕。
這件事很容易被讀成放寬——「不問了,那不就通過了?」正好相反。降級的方向是詢問 → 拒絕,原本可能被放行的案例,現在一律不執行。它把一個懸而未決的狀態,換成一個明確、可記錄、而且偏保守的結果。任務可能因此失敗,但失敗會立刻被看見,比卡 39 分鐘好處理。實測紀錄裡目前有 1 筆拒絕是由這個開關造成的。
同一筆決策還引了一份更早的歷史快照(2026-08-27 至 2026-09-07,2118 行):當時詢問 564 次、拒絕 531 次,而人工回應裡放行 54 次、拒絕 3 次。這是較早、較小的樣本,和上面那組最新數字屬於不同時間窗,兩邊不可互換引用。
那 94.7% 只成立在「有人回應的那 57 次」之內,不能外推到 564 次詢問。但在這個限定下它仍說明一件事:至少在有人回答的場合,詢問這一檔多半以放行收場;那麼在沒有人能回答的場合,把它留著只是把任務掛在那裡而已。
攔截當下還會推一則手機推播。這是刻意的:攔截如果只寫進紀錄,我要等到事後翻檔才知道;推播讓它在當下就有人知道。
這層做到的事情很具體:在動作發生之前判定,判定結果留下紀錄,而啟用無人值守開關之後,本來會跳詢問的情況會轉為直接拒絕。
但它有個前提我一直沒去驗——規則得先真的被掛上去。
還有一件事是這一層看不到的:它只看得到送出去的那道指令,看不到 session 本身的起訖與狀態——啟動、壓縮、收尾都不在它的視野裡。
攔截機制是靠「掛勾」生效的:某個事件發生時,工具會去讀設定、找到對應的腳本、執行它。設定檔被覆蓋、某一支腳本沒被註冊進去、啟動流程少跑一段,這層就等於不存在。而且失敗是安靜的:沒有掛上的規則不會報錯,只會什麼都不發生——紀錄裡不會多一列「我沒被掛上」。
我自己的設定檔就是每次啟動被整個覆蓋重設的。這個設計本身有理由(避免別處手改過的值殘留),但它也意味著:這層防線每一次啟動都要重新被組裝一次。組裝過程出錯,防線就從那一刻起消失,而我大概要等到出事才會發現。
那就是明天的題目:一個 session 從啟動到收尾,到底是誰在動什麼。
明日預告:Day 9|session 生命週期:從啟動到收尾誰在動