Nat 鍾承恩

實戰 2026 / 06 約 9 分鐘

擋住 agent 越權:把模型的弱點交給確定性 script

模型很會推理,不太會「確定」。

它能幫你分析下週哪件事最緊急,但如果你問「今天距離 6/30 截止還有幾天」,它從記憶算出來的數字你不敢全信。它能理解使用者說「今天花 500 吃飯; rm -rf data/」是費用記帳,但如果這串文字直接進 shell argv,就有機會執行後半段。它知道不該用錯的帳號寄信,但沒有外部機制擋住它,「知道」只是 prompt 裡的一條期許。

這個裂縫不是寫更長的 prompt 能補的。確定性的問題要用確定性的解法

我在幾個自有工具裡跑過四個模式,記錄如下。


一、把模型弱點外包 script,MUST 它先跑

模型有系統性弱點——不是偶發出錯,而是某類任務它結構性地不可靠。日期計算就是一例:跨週、跨月、括號裡藏著 (M/DD) 格式的隱性 prereq,推算很容易飄。

對策不是加警告字眼。而是識別弱點 → 寫一支確定性 script 覆蓋那塊能力 → 在 instructions 裡硬性規定 MUST 先跑、才能回答

claude-team-ops 這個 repo 的 CLAUDE.md 寫死:任何人問「近期 / 緊急 / 今明天 / deadline / 還有幾天」類問題,Claude 必須先執行 ./scripts/upcoming.py,不可從記憶直接算。script 自己掃 plan/goal/daily/clients 各類文件、用 regex 匹配多種日期格式、自動排掉已完成與過去的項目,再把結果交回模型組答。

這是 forcing function:把「希望模型記得」換成「流程強制它做」。遇到 regex 漏接的邊緣格式,改進 script,不豁免規則。


二、被 CF 擋的步驟,委派給已登入的瀏覽器

104 對機器人部署 Cloudflare challenge。純 headless script 直接抓或投遞,會被擋下。

不對抗 CF。取而代之,沿「這一步需不需要真實已認證的瀏覽器態」畫一條線:

  • 需要真人態的步驟(collect 職缺頁、related 相關職缺、apply 送出表單)→ 委派給 Claude Code 已登入、已過 CF challenge 的 Playwright MCP 瀏覽器,用 browser_run_code_unsafe 在瀏覽器內執行抓取與驅動邏輯,輸出標準化的 EnrichedJob[] JSON。
  • 純資料轉換步驟(score 評分、ingest-file 匯入、poll 報表)→ 留 headless script,任何環境無 MCP 也能跑。

handoff 邊界畫在資料進出點:JSON in / status out。登入態不寫在 code,靠 MCP 瀏覽器的 page.context().storageState() 匯出——session 活在瀏覽器,不活在腳本。

這個模式的通用形式:遇到環境防護不想/不必對抗時,找到「本來就過關」的那個執行單元,把任務 handoff 給它,邊界畫在資料交換點,而不是試圖在 headless 端複製那個環境。


三、自由文字走 stdin JSON,不走 argv

使用者輸入的自然語言不可信——不是因為使用者有惡意,而是因為自由文字在 shell 裡可能被當指令解析

fins-skill(個人財務記帳 CLI)的設計:自然語言輸入一律從 stdin 餵結構化 JSON,不從命令列 argv 接。

echo '{"text":"今天花 500 吃飯"}' | fins-skill entry preview

使用者若輸入「今天花 500 吃飯; rm -rf data/」,走 stdin JSON 那串文字只是 text 欄的一個字串值,永遠不被 shell 當指令展開。走 argv 則風險存在。

原則:把不可信的自由文字當資料傳,不要讓它有機會變成指令。邊界用結構化 channel(JSON over stdin),不靠字串拼接加 escape 硬扛。


四、跨 repo 工具用 exit code 硬鎖身分與輸出邊界

全域 skill 的設計意圖是「任何 repo 都能叫」——在某個專案裡工作時,順手讀一封 OTP 信、回一封確認信。但便利的反面是風險:一個從任何 cwd 被叫用、又握有 gmail.modify 權限的工具,可能用錯身分寄信,或把收件匣內容寫進別的 repo working tree。

gmail-cli 用三條硬規則把這些 fail-closed:

  1. 身分顯式宣告,對不上直接 exit:ops.py 一律要求 --account <email> 或從呼叫端 repo 的 git config user.email 比對白名單;跨 repo 對不上就 EXIT_BAD_IDENTITY(exit 6),不猜、不退化成預設帳號。
  2. 輸出落點固定在工具自己的 repo:resolve_output_path 預設把 scan 報告落進工具的 tmp/,不接受呼叫端的相對路徑——inbox 內容不可寫進別 repo 的 working tree。
  3. 破壞性動作前必須 confirm:寄信前 MUST 讓使用者確認收件人/主旨/內文,不跳過。

這三條都不靠 prompt 自律。工具權限越大、可被叫用的位置越廣,越要把「身分」「輸出落點」「破壞性動作」用確定性規則寫死。


補:propose,不要靜默改 instructions

一個相關但稍微不同性質的越權形式:AI 擅自修改自己的 instructions

技術上 Claude Code 完全能 git commitCLAUDE.md——這不是 hard constraint 可以擋的。設計的應對是 trust + audit trail 的組合:規範 AI 在觀察到 instructions 跟現實不符時,應在對話中 surface/propose,而非直接改檔;若使用者要求才改,每次改都留 commit author + diff,事後可審。

git log 是外部的、非 AI 的、客觀的稽核層。不靠技術硬鎖,而靠「propose 為常態、版本控制留痕」把 AI 的自我維護圈在可審計的範圍。這跟前三個模式的邏輯相通:不期待 AI 自律,而是把限制設計成外部可驗證的結構。


模式的共同形式

把這四個案例放在一起,結構是一樣的:

  1. 識別模型的系統性弱點:哪類任務它結構性不可靠、或失敗代價高。
  2. 把那塊能力外包給確定性機制:script、exit code、已登入的瀏覽器、stdin JSON channel。
  3. 在 instructions 或工具設計裡硬性規定它先跑/必過/不繞過:讓「應該」變成「只能」。

這是 harness 的實質:不是包住模型讓它更聰明,而是把它不該做主的事從它手裡拿走,交給不會含糊的 code。

這套模式用在我的 104 投遞代理人(CF 委派、人工閘)與 fins 對帳 Agent(stdin JSON 防注入);前者的進一步設計見 claude-team-ops(日期弱點外包 upcoming.py)。