Claude Code 的權(quán)限系統(tǒng)是如何工作的

在 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ì)上就是兩步:

  1. 先把當(dāng)前上下文里的 deny rulesask rules 展開成規(guī)則列表
  2. 再用 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 的處理可以概括成兩層:

  1. 先分別讀取每個 source 的原始配置
  2. 再按優(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ī)則更像:

  • Bash
  • Edit
  • mcp__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)系會更清楚:

  1. 輸入校驗
  2. 全局規(guī)則預(yù)檢查
  3. 工具級檢查
  4. mode / always allow 收口
  5. 形成最終 allow / ask / deny
  6. 執(zhí)行工具或生成拒絕結(jié)果
  7. 回寫工具結(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)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容