這篇是 2026 年 9 月 2 日補寫的(排程斷了幾天,回頭把日期補齊)。文中的版本號與「目前」指的是撰寫當日的官方文件狀態。

settings.json 存檔,關掉編輯器。deny 規則寫好了,看起來很合理。

然後你就停在一個有點尷尬的地方:你要怎麼知道它真的有在擋?

站上先前那篇〈Claude Code 權限系統完整教學〉講的是 allow、deny、ask 三種規則怎麼寫、* 怎麼用,那是把規則寫出來的部分。這篇不重講那些,這篇只談一件事:規則寫出來之後,它可能沒有涵蓋到你以為它涵蓋的範圍。定位是寫過規則的人回頭自檢,不是入門。

你能做的兩種驗證,各自只證明得了一小塊

想確認規則有效,直覺上有兩招。第一招是叫 Claude 跑一次你想擋的那個指令,看它會不會停下來問你。這招有用,但它只證明了一件事:你剛剛打的那一種寫法被擋住了。同一件事在 shell 裡通常有五六種寫法,你試的是其中一種。這跟「我 grep 不到就代表它不存在」是同一個毛病,證據的涵蓋範圍比你要下的結論小。

另一招是看 Claude Code 啟動時有沒有警告你。這招在特定情況下真的有用,2.1.246 版(npm 上是 2026 年 8 月 25 日發布)加了一條開機警告,專門提醒你「* 放在 subcommand 前面」的 allow 規則。但這是為一種特定形態量身訂做的警告,其他形態不會出聲。沒有警告不等於沒有問題。

兩招都不是驗證器,是抽樣。真正能收斂的方式是回去讀官方 permissions 文件,把它散在各個小節裡的警告收成一份清單。我讀完之後歸納出五種形態,下面逐一講。

先講清楚:以下全部出自官方文件與 CHANGELOG 的原文,我沒有實際跑過任何一條規則,也沒有驗證過任何一種漏擋。這篇的用途是讓你回頭檢查自己的設定,不是教你怎麼繞過別人的限制。

形態一:星號放的位置,決定了規則涵蓋多大

這一條最容易中,因為寫出來的東西看起來完全合理。

假設你想允許 Claude 對 main 分支做 git 操作,於是寫了 Bash(git * main)。看起來像是「git 相關的、跟 main 有關的指令」。實際上不是。文件講得很直白:Claude Code 把第一個 * 之前的東西照字面比對,所以 Bash(git log *) 只允許 git log,而 Bash(git *) 允許每一個 git 指令。你那條 Bash(git * main) 的星號站在 subcommand 前面,於是任何 git 子指令都落進涵蓋範圍。

官方原話:「Put the * after the subcommand. … Bash(git log *) allows only git log commands, and Bash(git *) allows every git command.」而 2.1.246 加的那條開機警告,正是為了這個形態。

判準只有一句:星號要放在「決定這個指令做什麼」的那個詞後面。 git 不決定要做什麼,git log 才決定。

形態二:文件自己說,用規則管參數是脆的

這一種更根本,不是你寫錯,是這條路本來就走不通。如果你寫了一條規則想約束某個指令的參數,例如只允許 curl 連某個網域,文件的評語是「Bash permission patterns that try to constrain command arguments are fragile」。fragile,脆弱。然後它老實地列出同一個請求可以怎麼換裝:選項的位置可以挪、協定可以換一種、可以讓伺服器幫你轉址、網址可以先塞進變數再展開、甚至多打一個空格就對不上了。這串清單的用途不是拿去用,是拿去數:你那條 curl 規則能擋住其中幾種?只要有一種擋不住,這條規則就不是你以為的那道牆。

這些不是漏洞,是 shell 的正常行為。你的規則比對的是那串字,而 shell 執行的是那串字被解析、展開之後的東西。這兩者從來就不是同一個東西。

正解不是把規則寫得更聰明。文件給的方向是換一個層次擋:與其用 Bash(...) 規則去約束 curl 的參數,不如整個 deny 掉 curlwget,改用 WebFetch(domain:...) 這種本來就以網域為單位的規則。你要管的是「連到哪裡」,那就用管得到「連到哪裡」的那套機制,不要用管「這句話怎麼寫」的那套。

形態三:規則放行的是指令,不是它把東西寫去哪

這一條是我讀文件時覺得最反直覺的。

拿門禁卡來對照。你的卡刷開了大樓正門,警衛看的是「這張卡有沒有權限進這棟樓」。至於你進去之後打開哪一扇門、把東西放進哪一個抽屜,那是另一套管制,跟大門讀卡機無關。

Bash(...) 規則就是那台讀卡機。它認的是「你這串指令長什麼樣」。而輸出重導向的目標是進門之後的事。文件寫得很清楚:Claude Code 會把 >>>2> 的目標當成一次檔案寫入來檢查,走的是另一套規則(Edit 規則、protected paths、working directories)。原文那句值得抄下來:「A rule such as Bash(git commit *) allows the command, not the target.」

文件還補了幾個細節:/dev/null 這個目標不會被檢查;以 ~ 開頭或含有 glob 字元的目標需要另外核准。

順著同一個邏輯,複合指令也是一樣的道理。Bash(safe-cmd *) 不會讓 safe-cmd && other-cmd 通過,因為文件明訂規則必須逐一比對每一個子指令。被認得的分隔符號有 &&||;||&& 以及換行。

形態四:讀檔這件事,可以長成很多不一樣的樣子

你寫了 Read()Edit() 的 deny 規則想保護某些檔案。這聽起來應該夠了。

問題在於「讀一個檔案」在 shell 裡不是只有一種形狀。< file 這種輸入重導向是在讀檔,tac 是在讀檔,egrep 也是在讀檔,但它們長得都不像 cat

就在今天發布的 2.1.257 版(npm 上是 2026 年 9 月 1 日),CHANGELOG 有這麼一條修正:「Fixed Bash Read()/Edit() deny rules not applying to < file redirects and reader commands like tac and egrep; a deny rule on any argument or redirect target now refuses the command」。

從這條修正紀錄可以往回推一件事:在這個版本之前,那些形式不吃 Read()/Edit() 的 deny 規則。這個推論是我從修正描述反推的,不是我驗證過的,但一條修正如果不是在補洞,就沒有存在的理由。如果你的環境還停在 8 月的版本,這是升級的理由之一。

形態五:你的規則和你的 shell,沒有在講同一種語言

最後一種最難防,因為它跟你寫得對不對無關。同一版 2.1.257 還修了兩件事。一件是:「Fixed Bash permission checks auto-approving certain [[ ]] conditionals that zsh parses differently from bash; these commands now prompt for approval」。[[ ]] 條件式在 zsh 跟 bash 底下的解析方式不一樣,於是有些情況被自動放行了。另一件是 auto mode 底下,permissions.ask 規則在複合指令或子 shell 裡會被跳過,該跳出來的確認提示沒有跳。

這兩件事指向同一個結論,也是整篇最值得帶走的一句:規則的安全性,取決於誰來解析這串字。 你寫的是一個文字比對規則,執行的是一個 shell 直譯器,中間隔著展開、引號、子 shell、方言差異。你以為你在描述「這件事准不准做」,你實際寫下的是「這句話准不准講」。

關掉 settings.json 之後,該做的三件事

回到開頭那個場景。存完檔,你其實有事可以做。

* 的位置先掃一遍。把你所有 allow 規則過一次,確認每一個星號都站在 subcommand 後面。這件事機械、快、而且是五種形態裡唯一有官方開機警告在幫你盯的。

「試圖約束參數」的那些規則挑出來,逐條問自己有沒有更上層的機制可以代替。網路存取用 WebFetch(domain:...),檔案保護用 Edit 規則與 protected paths。用管得到那件事的機制去管那件事。

然後去看一下你的 Claude Code 版本。形態四和形態五都是版本相關的,規則的實際語意會隨版本改變,這代表你的自檢有保存期限。今天檢查完是對的,不表示三個月後還是對的。

至於「怎麼知道規則真的有在擋」這個原始問題,誠實的答案是目前沒有一個乾淨的驗證器可以跑。能做的是把規則寫在對的層次上,讓它不需要靠巧妙的字串比對來成立。規則寫得越像在描述「意圖」而不是「字面」,你需要驗證的東西就越少。


參考來源