在 agent runtime 的實(shí)現(xiàn)里,權(quán)限從來不是外圍配置項,而是執(zhí)行系統(tǒng)的一部分。只要模型開始改文件、跑命令、調(diào)用外部工具,系統(tǒng)就必須回答一個核心問題:這一步是否允許執(zhí)行,以及這個判斷應(yīng)該在執(zhí)行鏈路的哪個位置完成。
Claude Code 的權(quán)限系統(tǒng)很適合作為分析樣本。它不是在工具外面額外包一層確認(rèn)框,而是把權(quán)限判斷直接嵌入工具調(diào)用鏈路,讓一次工具調(diào)用從發(fā)起到落地,始終伴隨一套可組合、可回寫的運(yùn)行時裁決。
文章從一個常見工作場景切入,分析權(quán)限系統(tǒng)實(shí)際解決的問題、內(nèi)部層次劃分,以及這些設(shè)計對自建 agent 系統(tǒng)的參考價值。
Claude Code 的權(quán)限系統(tǒng)大致是在回答 4 個問題:
- 這次工具請求本身成不成立
- 當(dāng)前上下文里,有沒有針對這個工具的全局規(guī)則
- 這個工具自己,是否還需要補(bǔ)充更細(xì)的風(fēng)險判斷
- 如果前面都沒有直接拍板,最后要怎么收口成真正的運(yùn)行時結(jié)果
為了先建立這個整體直覺,可以把 Claude Code 的做法先抽象成下面這樣一條執(zhí)行管線。先把它當(dāng)成一張“地圖”就夠了,具體函數(shù)名后面再一層層展開:
[圖片上傳失敗...(image-ac3df3-1775406003205)]
下面不按術(shù)語分塊,而是直接圍繞一個更貼近真實(shí)工作的場景往下走:
用戶說:“幫我修一下
src/runtime.ts里的一個小 bug,然后跑一次npm test?!?/p>
這類請求很有代表性,因為它同時包含兩種典型副作用:改文件,以及執(zhí)行命令。
1. 第一個問題不是“讓不讓做”,而是“這次請求到底成不成立”
假設(shè)模型先產(chǎn)出了一個文件編輯請求:
{
// 模型選擇的工具
"tool": "Edit",
"input": {
// 目標(biāo)文件路徑
"file_path": "src/runtime.ts",
// 預(yù)期被替換的原始內(nèi)容
"old_string": "if (x) return y",
// 希望寫入的新內(nèi)容
"new_string": "if (x != null) return y",
},
}
這時一個穩(wěn)妥的 runtime,通常不會立刻進(jìn)入“允許還是拒絕”的判斷,而是先做輸入校驗。對工程實(shí)現(xiàn)者來說,更重要的是這層職責(zé)本身:
- 參數(shù)結(jié)構(gòu)是否完整
- 路徑類型是否正確
- 這是不是一個連工具自己都無法理解的無效請求
原因很簡單。輸入本身如果不成立,它就不該進(jìn)入權(quán)限流程。否則你會把“壞請求”和“高風(fēng)險請求”混在一起,最后既難調(diào)試,也難向模型解釋失敗原因。
這里最重要的心智模型是:先區(qū)分“請求是否合法”,再區(qū)分“合法請求能否執(zhí)行”。
這不是為了講解方便才硬拆出來的層次,Claude Code 的工具抽象本身就是這么設(shè)計的。下面這段代碼是為了說明職責(zé)邊界做的精簡示意,去掉了泛型、UI 和錯誤處理等不影響主線的細(xì)節(jié):
// validateInput 的返回值只回答“輸入是否合法”
type ValidationResult =
| { result: true }
| { result: false; message: string; errorCode: number };
type Tool = {
// 先校驗輸入結(jié)構(gòu)和參數(shù)合法性
validateInput?(input, context): Promise<ValidationResult>;
// 再判斷這次調(diào)用是否允許執(zhí)行
checkPermissions(input, context): Promise<PermissionResult>;
};
這段代碼很短,但已經(jīng)說明了一件事:“輸入是否成立” 和 “這次動作是否允許執(zhí)行” 在 Claude Code 里從一開始就是兩個不同階段,而不是一個大而全的判斷函數(shù)。
2. 輸入合法之后,先做的是“全局規(guī)則預(yù)檢查”
編輯請求校驗通過以后,runtime 先做的不是立刻得出最終 allow / ask / deny,而是先跑一層比較通用的規(guī)則預(yù)檢查。對應(yīng)到 permissions.ts,比較核心的是這兩類判斷:
getDenyRuleForTool(...)getAskRuleForTool(...)
它們的核心邏輯并不復(fù)雜,本質(zhì)上就是兩步:
- 先把當(dāng)前上下文里的
deny rules或ask rules展開成規(guī)則列表 - 再用
toolMatchesRule(...)看這些規(guī)則里,是否存在一條能匹配“整個工具”的規(guī)則
這里還需要把“規(guī)則從哪里來”說清楚。對運(yùn)行時來說,getDenyRuleForTool(...) 和 getAskRuleForTool(...) 讀到的并不是硬編碼常量,而是從 settings 體系裝載出來的權(quán)限配置。
但對這一節(jié)來說,最重要的不是把配置系統(tǒng)的細(xì)枝末節(jié)一次講完,而是先理解這一層到底在回答什么問題:
當(dāng)前上下文里,有沒有針對整個工具的預(yù)設(shè)規(guī)則?
一個最小例子大致是這樣:
{
"permissions": {
// 這些工具命中后可直接放行
"allow": ["Read", "Glob"],
// 這些工具命中后必須先詢問
"ask": ["Bash", "WebFetch"],
// 這些工具命中后直接拒絕
"deny": ["Edit"],
},
}
這些配置可以來自幾類來源:
-
userSettings:全局用戶配置,通常是~/.claude/settings.json -
projectSettings:項目共享配置,通常是.claude/settings.json -
localSettings:項目本地配置,通常是.claude/settings.local.json -
policySettings:托管或企業(yè)級策略配置
2.1 多級配置如何合并
理解規(guī)則預(yù)檢查時,不能只看“配置寫在哪個文件里”,還要看這些文件最終是怎么合并成運(yùn)行時上下文的。這里保留一個夠用的心智模型就可以:
Claude Code 對 settings 的處理可以概括成兩層:
- 先分別讀取每個 source 的原始配置
- 再按優(yōu)先級把它們合并成一份生效視圖
對常見的文件型配置來說,核心優(yōu)先級是:
userSettings -> projectSettings -> localSettings -> policySettings
這里的含義不是“后面的文件會把前面的整個 permissions 對象覆蓋掉”,而是:
- 普通標(biāo)量字段,后面的值覆蓋前面的值
- 數(shù)組字段,會做拼接并去重
-
permissions.allow / ask / deny這類權(quán)限數(shù)組,屬于第二種情況
精簡后的合并邏輯大致可以寫成這樣:
function mergeArrays(targetArray, sourceArray) {
// 數(shù)組合并時不是覆蓋,而是拼接后去重
return uniq([...targetArray, ...sourceArray]);
}
function settingsMergeCustomizer(objValue, srcValue) {
// 只有數(shù)組走自定義合并
if (Array.isArray(objValue) && Array.isArray(srcValue)) {
return mergeArrays(objValue, srcValue);
}
// 其他字段交給默認(rèn) merge 行為處理
return undefined;
}
如果把它放到權(quán)限配置里理解,可以把結(jié)果看成這樣:
// userSettings
{ "permissions": { "ask": ["Bash"] } }
// projectSettings
{ "permissions": { "deny": ["Edit"] } }
// localSettings
{ "permissions": { "allow": ["Read", "Glob"], "ask": ["WebFetch"] } }
合并后的運(yùn)行時視圖會更接近:
{
"permissions": {
// 來自 localSettings
"allow": ["Read", "Glob"],
// ask 數(shù)組會疊加并去重
"ask": ["Bash", "WebFetch"],
// 來自 projectSettings
"deny": ["Edit"],
},
}
policySettings 稍微特殊一點(diǎn)。對理解本文主線來說,你只需要知道它代表更高優(yōu)先級的托管策略來源,最終也會一起進(jìn)入運(yùn)行時視圖。
因此,權(quán)限判斷里看到的 context 并不是某一個單獨(dú)文件,而是一份已經(jīng)經(jīng)過多級來源合并后的運(yùn)行時視圖。
運(yùn)行時會先把這些來源里的 permissions.allow / deny / ask 讀出來,再轉(zhuǎn)換成統(tǒng)一的 PermissionRule 列表。對權(quán)限判斷來說,可以把這個裝載過程理解成下面這樣:
function settingsJsonToRules(data, source) {
if (!data?.permissions) return [];
return ['allow', 'deny', 'ask'].flatMap(behavior =>
(data.permissions[behavior] || []).map(ruleString => ({
source,
ruleBehavior: behavior,
ruleValue: permissionRuleValueFromString(ruleString),
})),
);
}
也就是說,配置文件里的字符串規(guī)則會先被解析成統(tǒng)一結(jié)構(gòu),再進(jìn)入后面的匹配邏輯。設(shè)置來源本身也會被保留下來,因為后面不僅要判斷“命中了哪條規(guī)則”,還經(jīng)常要解釋“這條規(guī)則來自哪里”。
對應(yīng)的精簡代碼大致可以寫成這樣:
function getDenyRuleForTool(context, tool) {
// 先取出當(dāng)前上下文里的 deny 規(guī)則
// 再找第一條能匹配整個工具的規(guī)則
return (
getDenyRules(context).find(rule => toolMatchesRule(tool, rule)) || null
);
}
function getAskRuleForTool(context, tool) {
// ask 規(guī)則的處理方式完全對稱
return getAskRules(context).find(rule => toolMatchesRule(tool, rule)) || null;
}
這里最關(guān)鍵的是 toolMatchesRule(...) 這層語義:它檢查的是整工具匹配,不是帶內(nèi)容的細(xì)粒度匹配。也就是說,這一層看的規(guī)則更像:
BashEditmcp__server_name
而不是這類帶內(nèi)容的規(guī)則:
Bash(npm test:*)Edit(src/runtime.ts)
后者屬于更細(xì)粒度的內(nèi)容匹配,通常要等到后面的工具級檢查再處理。
比如:
- 整個
Edit工具是否被拒絕 - 整個
Bash工具是否要求先詢問 - 當(dāng)前會話里有沒有針對整類工具的默認(rèn)限制
這一層的特點(diǎn)是:它偏通用,按“工具整體”或“全局規(guī)則”先做一次早篩。
所以更準(zhǔn)確的說法不是“規(guī)則判斷之后系統(tǒng)就已經(jīng)知道最終是 allow 還是 deny 了”,而是:
- 如果這里命中整體驗證的
deny,可以直接結(jié)束 - 如果這里命中整體驗證的
ask,也可能直接要求確認(rèn) - 如果這里沒攔住,系統(tǒng)還要繼續(xù)往下看工具自己的語義檢查
這里需要特別區(qū)分的一點(diǎn)是:規(guī)則判斷是權(quán)限流程的一層,但它不是全部。
3. 接下來才是“工具級檢查”:tool.checkPermissions() 到底在做什么
假設(shè)第一步編輯已經(jīng)做完,模型接著又發(fā)起一個命令:
{
// 模型選擇執(zhí)行命令工具
"tool": "Bash",
"input": {
// 具體要執(zhí)行的命令
"command": "npm test -- permissions",
},
}
如果只靠上一節(jié)那層全局規(guī)則,通常還不夠,因為命令類工具的風(fēng)險不只來自“它是不是 Bash”,還來自“它實(shí)際會產(chǎn)生什么副作用”。
這時候就會進(jìn)入 tool.checkPermissions()。它的作用不是重復(fù)跑一遍全局規(guī)則,而是讓每個工具補(bǔ)上“只有我自己最清楚的風(fēng)險語義”。
這一層同樣會消費(fèi)配置文件,只不過它讀取的不再是“整工具規(guī)則”,而是更細(xì)粒度的內(nèi)容規(guī)則。對于 Bash 來說,配置里的規(guī)則可以寫成下面這種形式:
{
"permissions": {
// 整個 Bash 工具默認(rèn)先詢問
"ask": ["Bash"],
// 某些具體命令模式允許直接放行
"allow": ["Bash(npm test:*)", "Bash(git status:*)"],
// 某些具體命令模式直接拒絕
"deny": ["Bash(rm -rf:*)", "Bash(curl * | sh:*)"],
},
}
和第 2 節(jié)不同,這里看的不是 Bash 這個工具名本身,而是 Bash(...) 里面那段 ruleContent。運(yùn)行時會先把這些內(nèi)容規(guī)則按工具名分組,再交給工具自己的匹配邏輯處理。精簡后的結(jié)構(gòu)大致像這樣:
function getRuleByContentsForTool(context, tool, behavior) {
// 先篩出屬于這個工具、而且?guī)в?ruleContent 的規(guī)則
// 例如只取出 Bash(npm test:*) 這樣的內(nèi)容規(guī)則
return getRuleByContentsForToolName(
context,
getToolNameForPermissionCheck(tool),
behavior,
);
}
對 Bash 來說,后面的 matchingRulesForInput(...) 會繼續(xù)做一層工作:把這些內(nèi)容規(guī)則分成 deny / ask / allow 三組,再根據(jù)“精確匹配”或“前綴匹配”的方式去對比當(dāng)前命令。因此,第 3 節(jié)討論的不是“配置文件是否參與”,而是配置文件里的哪一類規(guī)則會在工具級檢查階段繼續(xù)生效。
更適合把它理解成“工具自己補(bǔ)充語義判斷”:
- 文件工具更關(guān)心路徑范圍、敏感文件和寫入目標(biāo)
- 命令工具更關(guān)心真實(shí)執(zhí)行內(nèi)容,而不只是字符串表面
- 網(wǎng)絡(luò)工具更關(guān)心目標(biāo)地址和數(shù)據(jù)外發(fā)風(fēng)險
這一步里,工具可能返回四類結(jié)果:
-
deny:工具自己已經(jīng)能確認(rèn)這次動作不能做 -
ask:工具自己認(rèn)為這次動作必須先確認(rèn) -
allow:工具自己確認(rèn)可以直接放行 -
passthrough:工具自己暫時不下結(jié)論,把判斷交回給通用權(quán)限流程繼續(xù)收口
這里的 passthrough 很關(guān)鍵,因為它解釋了你問的第二個問題:不是規(guī)則判斷一結(jié)束,就天然只剩 allow / ask / deny。 它不是“允許執(zhí)行”,也不是“出錯了”,而是“這個工具自己先不拍板,請上層繼續(xù)判斷”。在真實(shí)實(shí)現(xiàn)里,tool.checkPermissions() 經(jīng)常先返回 passthrough,然后后面的 mode、always allow 規(guī)則或者默認(rèn)提示邏輯再把它收斂成最終結(jié)果。
這也是為什么命令類工具總是最麻煩。npm test -- permissions 看起來像低風(fēng)險操作,但一旦換成帶重定向、子命令、復(fù)合命令的寫法,字符串表面就未必等于實(shí)際效果了。更克制的說法不是“它一定按某個固定順序做 8 步判斷”,而是:一個穩(wěn)妥的 runtime 會盡量先把命令整理到更容易分析的形式,再疊加工具自己的安全判斷。
如果先把這一層講成人話,它想表達(dá)的順序其實(shí)很簡單:
- 先看有沒有明確命中規(guī)則
- 再看這條命令本身有沒有更具體的風(fēng)險結(jié)構(gòu)
- 如果前面都沒攔住,再看有沒有補(bǔ)充放行條件
- 工具自己還是拿不準(zhǔn),就把判斷交回上層繼續(xù)收口
Bash 的權(quán)限檢查能把這種“分層遞進(jìn)”的結(jié)構(gòu)表現(xiàn)得比較完整。下面這段代碼保留了 bashPermissions.ts 的判斷順序,刪掉了建議規(guī)則、錯誤處理和邊界分支:
[圖片上傳失敗...(image-92acf-1775406003205)]
export const bashToolCheckPermission = (input, permissionContext) => {
// 先檢查最嚴(yán)格的“精確命中”規(guī)則
// 例如當(dāng)前命令正好命中 Bash(npm test -- permissions) 或 Bash(rm -rf /tmp/demo)
const exact = bashToolCheckExactMatchPermission(input, permissionContext);
if (exact.behavior === 'deny' || exact.behavior === 'ask') return exact;
// 再檢查更寬一點(diǎn)的前綴規(guī)則
// 例如 Bash(npm test:*)、Bash(git status:*)、Bash(rm -rf:*)
const { matchingDenyRules, matchingAskRules, matchingAllowRules } =
matchingRulesForInput(input, permissionContext, 'prefix');
// deny / ask 一旦命中,立即結(jié)束
// 例如 Bash(rm -rf:*) 命中 deny,或 Bash(npm publish:*) 命中 ask
if (matchingDenyRules[0]) return { behavior: 'deny' };
if (matchingAskRules[0]) return { behavior: 'ask' };
// 然后檢查路徑約束,例如是否越過工作區(qū)、是否碰到敏感路徑
// 例如 cat ../secrets.txt、echo hi > .claude/settings.json
const pathResult = checkPathConstraints(input, getCwd(), permissionContext);
if (pathResult.behavior !== 'passthrough') return pathResult;
// 允許規(guī)則要在前面的風(fēng)險檢查之后再生效
// 例如精確規(guī)則明確允許 Bash(npm test -- permissions)
if (exact.behavior === 'allow') return exact;
// 例如前綴規(guī)則允許 Bash(npm test:*) 或 Bash(git status:*)
if (matchingAllowRules[0]) return { behavior: 'allow', updatedInput: input };
// 最后再看 mode 和“只讀命令”這樣的補(bǔ)充放行條件
// 例如在 bypassPermissions 模式下直接放行,或在受限模式下要求 ask
const modeResult = checkPermissionMode(input, permissionContext);
if (modeResult.behavior !== 'passthrough') return modeResult;
// 只讀命令可以作為最后一道補(bǔ)充放行
// 例如 ls -la、pwd、git status
if (BashTool.isReadOnly(input)) {
return { behavior: 'allow', updatedInput: input };
}
// 工具自己沒有最終意見時,交回通用權(quán)限流程繼續(xù)收口
// 例如 npm test 既沒命中 allow/deny/ask,也不是純只讀命令
return { behavior: 'passthrough' };
};
這段代碼最值得關(guān)注的不是語法,而是判斷順序本身:
- 先看最明確的規(guī)則命中
- 再看路徑和命令語義這類工具特有風(fēng)險
- 最后才落到 mode 和只讀判斷
- 如果工具自己也沒有明確放行或拒絕,就先返回
passthrough
這也是“工具級檢查”真正的意義。它不是重復(fù)造一遍全局權(quán)限系統(tǒng),而是把那些只有工具自己最清楚的風(fēng)險補(bǔ)上。
3.1 為什么 Shell 權(quán)限判斷不能只看字符串
之所以這里要專門展開,是因為命令工具的“內(nèi)容規(guī)則匹配”只有建立在更可靠的語法理解上,前面那套判斷順序才真正站得住。
在 Bash 這類工具里,光靠字符串切分通常不夠,因為很多 Shell 語義只有在語法層面才能穩(wěn)定區(qū)分。tree-sitter 在這里的作用,不是簡單地把命令拆成 token,而是把命令解析成 AST,讓系統(tǒng)能看到更可靠的結(jié)構(gòu)信息,比如:
- 有沒有真正的操作符節(jié)點(diǎn),如
&&、||、; - 有沒有 pipeline、subshell、command substitution、heredoc
- 某段內(nèi)容是在引號里、在參數(shù)里,還是已經(jīng)構(gòu)成了獨(dú)立命令結(jié)構(gòu)
這類信息直接影響權(quán)限判斷。下面這組例子更能說明問題:
# 這些都包含 rm,但語義完全不同
rm -rf / # 真正執(zhí)行刪除,極度危險
echo "rm -rf /" > log.txt # 寫入字符串,本質(zhì)上不是執(zhí)行 rm
grep "rm" history.txt # 搜索文本,本質(zhì)上不是執(zhí)行 rm
find . -name '*.tmp' -exec rm {} \; # rm 出現(xiàn)在 find -exec 結(jié)構(gòu)里
如果只做字符串匹配,這幾條命令都可能因為包含 rm 而被粗暴歸成一類;但從語法結(jié)構(gòu)看,它們分別是:
- 一個真正的危險刪除命令
- 一個帶重定向的
echo - 一個文本搜索命令
- 一個
find -exec復(fù)合結(jié)構(gòu)
再看另一組例子:
cd src && npm test # 頂層操作符,表示兩條串聯(lián)命令
echo 'cd src && npm test' # 只是字符串,不是串聯(lián)執(zhí)行
echo $(git status --short) # 含 command substitution,內(nèi)部仍有可執(zhí)行結(jié)構(gòu)
這里如果只按字符去找 && 或 $(,也很容易誤判。真正有用的不是“字符串里出現(xiàn)了什么符號”,而是這些符號在語法樹里扮演什么角色。
tree-sitter 的價值就在于,它讓系統(tǒng)能按語法結(jié)構(gòu)理解 Shell,而不是只靠字符串切分或正則近似猜測。對于權(quán)限判斷來說,這意味著系統(tǒng)判斷的不是“這串字符像不像危險命令”,而是“這條 Shell 語句在語法上到底由哪些結(jié)構(gòu)組成”。
4. 經(jīng)過收口之后,系統(tǒng)才形成最終的 allow / ask / deny
前兩節(jié)講完以后,權(quán)限流程其實(shí)還沒結(jié)束。Claude Code 在 hasPermissionsToUseToolInner(...) 里還會繼續(xù)做兩類收口:
- mode 是否允許直接繞過后續(xù)權(quán)限提示,比如當(dāng)前處在
bypassPermissions - 整個工具是否命中了 always allow 這類整體放行規(guī)則,比如
toolAlwaysAllowedRule(...)
如果前面 tool.checkPermissions() 返回的是 passthrough,后面的收口邏輯還會把它統(tǒng)一轉(zhuǎn)換成 ask。更準(zhǔn)確的理解應(yīng)該是:
最終的 allow / ask / deny,是整條權(quán)限管線收斂后的結(jié)果,不是某一層單獨(dú)拍板的結(jié)果。
這時最值得記住的不是內(nèi)部函數(shù)名,而是這三個運(yùn)行時結(jié)果:
-
allow:直接執(zhí)行 -
ask:暫停并向用戶確認(rèn) -
deny:不執(zhí)行,直接生成拒絕結(jié)果
這三種結(jié)果里,最容易被誤解的是 ask。它不是失敗,更像“當(dāng)前上下文不足以自動放行,所以把決定權(quán)拋回給用戶”。如果用戶確認(rèn)了,請求可以重新回到同一條執(zhí)行管線;如果用戶拒絕,系統(tǒng)就把這次動作收束為一次明確的拒絕。
換句話說,權(quán)限系統(tǒng)不是“執(zhí)行前攔一下”這么簡單,它更像一個會改變后續(xù)推理狀態(tài)的 runtime 分叉點(diǎn)。
5. 為什么“回寫工具結(jié)果”這一步不能省
很多新人理解權(quán)限系統(tǒng)時,只盯著前半段:怎么攔,怎么放。真正讓 agent 能繼續(xù)工作的,往往是后半段:這次動作的結(jié)果要怎么回到模型上下文里。
繼續(xù)沿著這個場景看:
- 如果文件改動成功,模型需要知道“改好了”,這樣它才會繼續(xù)決定要不要跑測試、要不要總結(jié)改動。
- 如果測試命令被拒絕,模型也需要收到一個結(jié)構(gòu)化結(jié)果,知道“為什么沒跑成”,以及下一步更適合改成什么動作。
更合理的 runtime 會把這類結(jié)果寫回消息流。這里同樣不必把實(shí)現(xiàn)細(xì)節(jié)寫得過重,抓住抽象就夠了:無論是執(zhí)行成功、等待確認(rèn),還是被拒絕,系統(tǒng)都應(yīng)該把結(jié)果結(jié)構(gòu)化地送回模型。
這一段的重點(diǎn)也不是讓你記字段名,而是看清楚:回寫結(jié)果至少要把“有沒有執(zhí)行”“為什么沒執(zhí)行”“原因來自哪一層判斷”這幾類信息帶回去。
以“命令被拒絕”為例,回寫給模型的結(jié)果大致可以理解成下面這種結(jié)構(gòu):
{
// 這次工具調(diào)用最終沒有執(zhí)行
"behavior": "deny",
// 給模型看的拒絕說明
"message": "Permission to use Bash with command rm -rf / has been denied.",
"decisionReason": {
// 拒絕是由規(guī)則觸發(fā)的
"type": "rule",
"rule": {
// 規(guī)則來源,例如 localSettings / projectSettings / session
"source": "localSettings",
// 這是一條 deny 規(guī)則
"ruleBehavior": "deny",
"ruleValue": {
// 作用于 Bash 工具
"toolName": "Bash",
// 命中的內(nèi)容規(guī)則
"ruleContent": "rm -rf:*",
},
},
},
}
這個結(jié)構(gòu)的關(guān)鍵不在字段名本身,而在它把三件事都明確帶回了模型:
- 這次動作沒有執(zhí)行
- 沒執(zhí)行的原因是什么
- 原因來自哪一類規(guī)則或判斷
這樣模型下一步才能基于失敗原因繼續(xù)推理,比如改用更安全的命令、向用戶申請確認(rèn),或者直接解釋為什么這一步被攔住了。
如果沒有這一步,模型只會感知到“動作沒發(fā)生”,卻不知道是輸入無效、權(quán)限不足,還是用戶剛剛拒絕了它。后續(xù)行為就容易發(fā)散。
6. 用一段精簡過的真實(shí)代碼,把這條權(quán)限管線串起來
前面看的是某個具體工具內(nèi)部怎么細(xì)化判斷。再往上一層,Claude Code 在通用權(quán)限管線里也有一個很清楚的結(jié)構(gòu)。下面這段代碼同樣是為了解釋主流程做的精簡示意,保留的是核心決策順序,并且和開頭那張流程圖一一對應(yīng):
async function hasPermissionsToUseToolInner(tool, input, context) {
// 1. 全局規(guī)則預(yù)檢查:整個工具是否被 deny
const denyRule = getDenyRuleForTool(context, tool);
if (denyRule) {
return { behavior: 'deny', decisionReason: { type: 'rule' } };
}
// 2. 全局規(guī)則預(yù)檢查:整個工具是否要求先 ask
const askRule = getAskRuleForTool(context, tool);
if (askRule) {
return { behavior: 'ask', decisionReason: { type: 'rule' } };
}
// 3. 工具級檢查:交給工具自己補(bǔ)充語義判斷
// 注意:這里先把輸入解析成工具期望的結(jié)構(gòu)
const parsedInput = tool.inputSchema.parse(input);
const toolPermissionResult = await tool.checkPermissions(
parsedInput,
context,
);
// deny / ask 會直接作為最終結(jié)果向上返回
if (toolPermissionResult.behavior === 'deny') return toolPermissionResult;
if (toolPermissionResult.behavior === 'ask') return toolPermissionResult;
// 4. 收口:mode 是否允許直接放行
if (context.mode === 'bypassPermissions') {
return { behavior: 'allow', updatedInput: parsedInput };
}
// 5. 收口:整個工具是否命中 always allow
if (toolAlwaysAllowedRule(context, tool)) {
return { behavior: 'allow', updatedInput: parsedInput };
}
// 6. 最終裁決:passthrough 會被收口成 ask
// 只有真正明確 allow 的結(jié)果,才會在這里繼續(xù)保留為 allow
return {
behavior: toolPermissionResult.behavior === 'passthrough' ? 'ask' : 'allow',
updatedInput: parsedInput,
};
}
如果把開頭的流程圖和這段代碼對照著看,關(guān)系會更清楚:
- 輸入校驗
- 全局規(guī)則預(yù)檢查
- 工具級檢查
- mode / always allow 收口
- 形成最終
allow / ask / deny - 執(zhí)行工具或生成拒絕結(jié)果
- 回寫工具結(jié)果
你會發(fā)現(xiàn),真正決定行為的不是某一個單點(diǎn)函數(shù),而是幾層判斷逐步收斂。這樣設(shè)計的好處是,每一層都只負(fù)責(zé)自己最擅長的那部分語義,最后再把結(jié)果收成統(tǒng)一的運(yùn)行時裁決。
總結(jié)
如果只看表面現(xiàn)象,Claude Code 的權(quán)限系統(tǒng)像是在工具外面加了一層“允許 / 拒絕”的開關(guān);但順著執(zhí)行鏈路拆開以后,會發(fā)現(xiàn)它實(shí)際是一個分層運(yùn)行的判斷過程:先做輸入校驗,再做全局規(guī)則預(yù)檢查,再進(jìn)入工具級檢查,最后把結(jié)果收口成 allow / ask / deny,并把結(jié)果回寫給模型。
這套機(jī)制的關(guān)鍵,不在某一個單獨(dú)函數(shù),而在這些層次的分工。配置文件里針對整工具的規(guī)則、帶內(nèi)容的細(xì)粒度規(guī)則、Bash 自己的語義判斷,以及最終裁決后的結(jié)果回寫,一起構(gòu)成了完整閉環(huán)。也正因為如此,權(quán)限系統(tǒng)討論的從來不只是“能不能執(zhí)行”,還包括“為什么被攔住”“攔住之后模型會收到什么”。
從這個角度看,Claude Code 的權(quán)限系統(tǒng)并不是一個獨(dú)立的安全補(bǔ)丁,而是工具執(zhí)行系統(tǒng)的一部分。它和配置加載、Shell 解析、規(guī)則匹配、結(jié)果回寫共同組成了一條完整的執(zhí)行管線;理解這一點(diǎn),也就能更準(zhǔn)確地理解前面這些判斷步驟為什么要以現(xiàn)在這樣的順序出現(xiàn)。