「全部不准動,只開這個資料夾」:Claude Code 的 deny 寫不出這句話,opencode 可以
寫於 2026 年 8 月 17 日,9 月才上線(部落格的發佈額度在 8 月中用完了,稿子積壓了三週)。文中對兩邊權限系統的描述以 8 月 17 日的官方文件為準,你讀到時文件可能已經改版。
需求很小:這個 repo 我只想讓 agent 改 docs/ 底下的文件,其他檔案一律不准動。但 Bash 我希望維持原樣,該問我的還是要問。
聽起來像設定檔改兩行就能解決的事。我在 Claude Code 的 permissions 裡繞了不少路才發現,這句話用規則寫不出來。
第一個想法是最直覺的那個,它不會動
先寫「全部拒絕」,再開一個例外:
1 | { |
這份設定的行為是:docs/ 底下也不能改。官方文件把這件事寫得很白:
A broad deny rule like
Bash(aws *)blocks every matching call, including calls that also match a narrower allow rule likeBash(aws s3 ls), so a deny rule can’t carry allowlist exceptions.
deny 沒有例外清單這種東西。它比較的不是「哪條規則更精確」,而是「有沒有 deny 命中」。命中就結束了,allow 寫得再細也不會被看到。
ask 也是同一套。一條 ask 規則命中,就算另一條更精確的 allow 也命中,它還是會跳出來問你。這個設計很好懂:在權限系統裡,「更精確」不是一種特權。
順帶講一個我當時沒注意、後來吃過虧的區別。deny 寫成裸工具名跟寫成帶路徑的規則,效果不一樣:
A bare tool name like
Bashremoves the tool from Claude’s context entirely, so Claude never sees it. A scoped rule likeBash(rm *)leaves the tool available and blocks matching calls when Claude attempts them.
deny: ["Bash"] 是把 Bash 從工具清單裡整個拔掉,模型根本不知道有這個工具。deny: ["Bash(rm *)"] 是工具還在,只是 rm 那類呼叫會被擋。前者是「這個能力不存在」,後者是「你可以試,但會被攔」。對模型行為的影響差很多:能力被拔掉的時候,它會想別的辦法達成同一件事;能力還在只是被擋,它通常會回頭問你。
反過來寫,卡在 allow 不吃 wildcard
既然 deny 開不了例外,那就反過來:只 allow 我要的,其他都不要 allow。
問題是「其他都不要 allow」在 Claude Code 裡不是一條規則能表達的狀態。沒被 allow 命中的呼叫不會被拒絕,它會跳出來問你。這是預設模式的行為,不是漏洞。
我試過在 allow 裡放 wildcard 想反向操作,文件明講這條路是封的:
An unanchored allow glob such as
"*","B*", or"mcp__*"is skipped with a warning and doesn’t auto-approve anything.
allow 的 glob 只能用在 mcp__<server>__ 這個前綴後面,例如 mcp__github__get_*。工具名位置放裸 * 會被跳過,還會給你一個啟動警告。deny 和 ask 反而可以:"*" 在 deny 裡匹配每一個工具。
所以三種規則的能力是不對稱的。deny 和 ask 能全域掃,allow 不能。這個不對稱是刻意的,方向也很合理:會讓權限變鬆的規則,不給你一次寫完的捷徑。
真正做得到的做法,代價在別的地方
要達成「預設拒絕、少數例外」,Claude Code 的答案不在規則裡,在模式裡。dontAsk 模式的定義是這樣:
Auto-denies tools unless pre-approved via
/permissionsorpermissions.allowrules.
配上 allow: ["Edit(docs/**)"],就是我要的效果了。沒預先允許的一律自動拒絕,不問你。
但這個模式是整個 session 的。它一開,Bash、Read、WebFetch 全部跟著變成「沒預先允許就拒絕」。我原本的需求後半句是「Bash 維持該問我就問」,那半句在這裡消失了。想留住它,我得把 Bash 該允許的東西也全部列進 allow,等於把整個工作流的白名單先寫完一次。
另一條路是 PreToolUse hook。exit code 2 就擋掉,而且它擋得比規則早:
A hook that exits with code 2 stops the tool call before permission rules are evaluated, so the block applies even when an allow rule would otherwise let the call proceed.
這條真的做得到逐工具的精細控制。代價是你開始寫程式了。一個「只准改 docs」的需求,最後產出一支要維護、要處理路徑正規化、還得自己想清楚 symlink 怎麼算的 shell script。
opencode 把同一題丟回設定檔
opencode 的權限設定長這樣,官方文件的範例我原封不動抄過來:
1 | { |
看 edit 那一段。"*": "deny" 之後跟著一條 allow,而它是有效的。因為 opencode 的規則評估方式是「最後符合者勝」(規則按 last match wins 原則評估)。
這個差別攤開來看,其實是同一個問題的兩種答案:
Claude Code 選的是「拒絕最強」。好處是 deny 是硬保證,你在 managed settings 裡放一條 deny,底下沒有任何一層設定、任何一個命令列參數能鬆綁它,文件寫得很明白:no other level, including command line arguments, can override a managed permission rule。壞處就是我前面撞到的,硬保證不能有例外,因為「例外」跟「硬保證」在定義上就打架。
opencode 選的是「順序決定」。好處是「預設關、少數開」跟「預設開、少數關」都是同一套語法寫得出來,而且是逐工具寫的:bash 預設問你、edit 預設拒絕,兩者互不干涉。壞處是 deny 不再是硬保證了。你的 deny 有沒有效,取決於它下面還有沒有別的規則。多人共用一份設定、或者設定分好幾層合併的時候,這件事會變得需要動腦。
我認為這個取捨沒有標準答案,但有適用邊界:規則要拿來當合規邊界,就要 deny-first;規則要拿來當日常工作流的方便設定,順序制好用得多。Claude Code 的權限系統從一開始就在往前者長,managed settings、disableBypassPermissionsMode、把 MCP 工具標成 requiresUserInteraction 這些設計全部指向同一個方向。
兩個我在 Claude Code 找不到對應項的權限
opencode 的權限清單裡有兩項讓我停下來看了一下。
一個是 doom_loop,它的定義是「相同工具呼叫重複 3 次(輸入相同)」,預設值是 ask。也就是說,agent 卡在同一個指令上來回鬼打牆這件事,在它這裡是一個可以被權限系統攔下來的事件類型,跟「要不要允許 rm」放在同一張表上管理。
另一個是 question,「執行期間詢問使用者」本身是一項權限。你可以把它設成 deny,效果是禁止 agent 反過來問你問題。
Claude Code 這兩件事的處理方式不太一樣。「限制提問」是有的,但它是模式的附帶效果:dontAsk 模式底下,AskUserQuestion 會被拒絕,而且是「即使你已經允許它也照樣拒絕」。你不能單獨關掉提問而保留其他行為。至於重複呼叫偵測,我在權限文件那一整頁裡沒找到對應的設定項。
這裡要誠實一點:我查的是 permissions 那一頁,沒找到不等於整個產品沒有。這種偵測完全可以做在模型端或 harness 內部而不出現在權限文件裡,我沒有辦法從一頁文件反證它不存在。我能說的只有:它不是一個你設定得到的權限項。
順手記三個踩得到的坑
查文件的過程撿到幾個跟主題無關但值得知道的東西,都是 Claude Code 這邊的。
給 Write 寫路徑規則沒有用。文件寫得很直接:Write、NotebookEdit、Glob、MultiEdit 的路徑規則會被接受,但永遠不會被查,只在啟動時給你一個警告。要管檔案路徑,一律寫 Edit(docs/**) 和 Read(docs/**)。這個坑陰險的地方在於設定檔看起來完全正常。
Read 的 deny 會連帶擋掉 Edit 和 Write,包含在那個路徑下建新檔。但 NotebookEdit 不在覆蓋範圍內,.ipynb 要另外補一條 Edit deny。
還有一個範圍問題值得記住:Read 和 Edit 的 deny 管得到 Claude 內建的檔案工具,也管得到它認得出來的 bash 檔案指令(cat、head、tail、sed 這些),但管不到你叫它跑的 Python 或 Node script 自己開檔案。要 OS 層級的強制,得去開 sandbox。權限規則攔的是「Claude 主動要求做的事」,不是「Claude 起的子行程做的事」。這兩件事的差距,在你以為某個路徑被鎖住的時候特別重要。
誠實邊界
Claude Code 是我每天在用的工具,上面講的行為有些是踩過才知道的。opencode 我沒有裝、沒跑過一次 session,這篇裡所有關於它的描述都出自官方文件(opencode.ai/docs),我只讀了文件。實際跑起來 last match wins 在多層設定合併時怎麼算、doom_loop 的三次計數是怎麼判「輸入相同」,我沒有第一手資料可以說。
順帶更正一件我自己的認知:這個專案的 repo 現在掛在 anomalyco/opencode 底下,不是 sst/opencode。查的時候我是用 gh api repos/sst/opencode,回來的 full_name 直接是新的那個,MIT 授權、未封存、當天還有 push,最新一版是 8 月 13 日的 v1.18.18。星數 198,359。
回到一開始那個 repo
現在我會怎麼處理?如果只是我自己一個人做事,寫一支 PreToolUse hook 就解掉了,二十行的事。
但如果這條規則的目的是「保證不會被改」,那 hook 是錯的選擇。hook 可以被 --settings '{"disableAllHooks": true}' 關掉,而 managed settings 裡的 deny 不行。這時候該接受 deny 沒有例外,把整個需求重寫成「哪些路徑絕對不能動」,而不是「只有哪些路徑可以動」。
換句話說,我原本那句需求的形狀,本身就選錯了工具。要「白名單」語意就別用 deny,deny 天生是黑名單。這件事在 opencode 那邊會被順序制的彈性藏起來,直到某天你的設定合併順序變了才發現。
參考來源










