在 CLI Agent 里挂 MCP server
在终端 Agent 里挂 MCP server 的两种方式 —— claude mcp add 命令 vs settings.json 配置文件,含 stdio 与远程 server 的踩坑要点
理解 Claude Code 的 settings 三层模型(用户级 / 项目级 / local)的合并规则、覆盖优先级与最常踩的坑
内容摘要
Claude Code 的所有配置——能允许哪些工具、敏感目录要不要拦、用哪个模型、要不要挂钩子、要不要自动加 `Co-Authored-By`——最终都汇聚到一个名字叫 `settings.json` 的 JSON 文件里。但这个文件不止一份。Claude Code 把它**沿用户、项目、个人覆盖三层叠起来**,叫法不严格统一,本卡用三个固定的中文称呼:
Claude Code 的所有配置——能允许哪些工具、敏感目录要不要拦、用哪个模型、要不要挂钩子、要不要自动加 Co-Authored-By——最终都汇聚到一个名字叫 settings.json 的 JSON 文件里。但这个文件不止一份。Claude Code 把它沿用户、项目、个人覆盖三层叠起来,叫法不严格统一,本卡用三个固定的中文称呼:
~/.claude/settings.json。<repo>/.claude/settings.json。<repo>/.claude/settings.local.json。在团队 / 企业场景之上,还有一层 企业 managed settings,由 IT 推下来强制生效,普通用户不能改、也不能覆盖;放在 macOS 的 /Library/Application Support/ClaudeCode/managed-settings.json、Linux 的 /etc/claude-code/managed-settings.json、Windows 的 C:\Program Files\ClaudeCode\managed-settings.json(也支持 MDM / 注册表)。本卡聚焦个人/团队最常打交道的那三层,企业 managed 只在"覆盖优先级"那张图里提一下。
为什么要分这么多层?核心动机就一个:让"团队约定"和"个人偏好"互不打架,又能互相补强。比如团队希望所有人都禁止 curl 工具直接访问外网(写在项目级),但你个人喜欢用 iTerm2 + acceptEdits 模式跑得更顺手(写在 local),而你所有项目都希望默认显示中文(写在用户级)。三层叠起来,互相不冲突。
阅读这张图的两条规则:
model)由高优先级的那层赢;对象字段(如 env)逐键合并;数组字段(如 permissions.allow / permissions.deny)会被拼接 + 去重,不是覆盖。这一点最容易踩坑,单独抽出来在"常见误区"里讲。| 要素 | 三层各自的位置 | 适合放什么 |
|---|---|---|
| 用户级 | ~/.claude/settings.json | 跨项目通用偏好:默认 model、outputStyle、includeCoAuthoredBy、个人想全局放行的 permissions.allow(比如 Bash(ls *)) |
| 项目级 | <repo>/.claude/settings.json | 团队约定:项目硬性 permissions.deny(拦 .env、拦 curl)、项目的 hooks、绑定的 model、是否自动信任 .mcp.json |
| local 级 | <repo>/.claude/settings.local.json | 个人临时放行:试某个新工具时的一次性 allow、本地特殊路径(Read(/Volumes/...))、个人想覆盖团队默认的 defaultMode |
| 企业 managed | /Library/.../managed-settings.json 等 | 公司硬性策略:白名单模型 availableModels、强制登录方式 forceLoginMethod、allowManagedHooksOnly |
# 用户级(全局,所有项目共用)
ls -la ~/.claude/settings.json
# 项目级(要在仓库根目录跑)
ls -la .claude/settings.json
# local 级(同样在仓库根目录)
ls -la .claude/settings.local.json
如果文件不存在很正常——Claude Code 第一次需要写入时会自动创建。.claude/settings.local.json 在被首次创建时,Claude Code 会自动把它追加到项目的 .gitignore,不需要你手动 ignore,但养成检查习惯没坏处:
grep -n "settings.local.json" .gitignore || echo "未找到,建议手动加一行 .claude/settings.local.json"
进 Claude Code 内部,输入:
/status
/status 会列出所有 settings 来源(包括是否有企业 managed),帮你确认哪一层在生效。要交互式调配置则用:
/config
/config 提供菜单式编辑,所有改动落到对应的 JSON 文件里。
示例 A:用户级(所有项目通用偏好)
// ~/.claude/settings.json
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"model": "claude-sonnet-4-6",
"outputStyle": "concise-zh",
"permissions": {
"allow": [
"Bash(ls *)",
"Bash(cat *)",
"Read(~/.zshrc)"
],
"defaultMode": "default"
},
"attribution": {
"commit": "Co-Authored-By: Claude <[email protected]>"
}
}
示例 B:项目级(团队共享,进 git)
// <repo>/.claude/settings.json
{
"permissions": {
"deny": [
"Bash(curl *)",
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)"
],
"allow": [
"Bash(npm run lint)",
"Bash(npm run test *)",
"Bash(npm run build)"
]
},
"hooks": {
"PreToolUse": [
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "scripts/log-bash.sh" }] }
]
},
"enabledMcpjsonServers": ["filesystem", "github"]
}
示例 C:local 级(仅你这台机器,不进 git)
// <repo>/.claude/settings.local.json
{
"permissions": {
"allow": [
"Bash(docker compose *)",
"Read(/Volumes/external-dataset/**)"
],
"defaultMode": "acceptEdits"
},
"env": {
"MY_LOCAL_DEBUG_FLAG": "1"
}
}
合并以后实际生效:
model 取用户级的 claude-sonnet-4-6(项目和 local 都没改)。permissions.deny 完全保留项目级那 4 条(用户和 local 都没写 deny)。permissions.allow = 用户的 3 条 + 项目的 3 条 + local 的 2 条,8 条全部生效,去重之后即是结果。defaultMode = local 的 acceptEdits 赢(覆盖了用户级的 default)。env.MY_LOCAL_DEBUG_FLAG=1 生效。不同终端 Agent 都有自己的"全局 vs 项目"配置模型。理解差异有助于在多工具切换时不踩坑:
| 维度 | Claude Code | Codex CLI(OpenAI) | Gemini CLI(Google) | Cursor |
|---|---|---|---|---|
| 配置文件名 | settings.json | config.toml | settings.json | .cursor/rules/*.mdc + Settings UI |
| 用户级路径 | ~/.claude/settings.json | ~/.codex/config.toml | ~/.gemini/settings.json | Cursor 应用内 Settings → User Rules |
| 项目级路径 | <repo>/.claude/settings.json | <repo>/.codex/config.toml(会向上递归查找) | <repo>/.gemini/settings.json | <repo>/.cursor/rules/*.mdc |
| 个人/local 层 | ✅ settings.local.json(自动 gitignore) | ⚠️ 只能用 -c 命令行覆盖或额外 profile | ❌ 没有独立 local 文件 | ❌ 无独立 local 概念 |
| 企业/managed 层 | ✅ macOS plist / Linux /etc / Windows 注册表 | ✅ requirements.toml(强约束)+ managed_config.toml(默认值) | ✅ system overrides 作为最高优先级 | ✅ Team Rules(仅 Team/Enterprise) |
| 数组字段合并行为 | 拼接 + 去重 | 同名键由优先级高的覆盖(按 toml key) | 数组合并、mcpServers 同名取高优 | 规则文件叠加进 system prompt |
| 项目层是否受信任开关保护 | ❌ 默认信任 | ✅ 项目未 trust 时跳过 .codex/ 层 | ⚠️ 取决于工作区信任 | ⚠️ 取决于工作区信任 |
核心区别用一句话说:Claude Code 是"个人 / 团队 / 个人覆盖团队"三明治结构,且 local 层默认 gitignore;Codex 没有专门 local 层,靠 profile + -c 替代;Gemini 走"系统覆盖一切"路线,企业管控更强;Cursor 没有标量 settings 合并问题,规则全部叠加进 prompt。
迁移建议:从 Cursor 转 Claude Code 的人最容易把所有规则塞进项目级,忘了 local;从 Codex 转过来的人则容易忽略"数组合并 vs 字段覆盖"这条差异。
| 误区 | 准确理解 |
|---|---|
| 以为项目级和用户级是"二选一"——配了项目就不读用户 | 三层一直都在叠加。即便项目级写了 model,用户级里的 outputStyle、env、hooks 仍然生效,只是同名字段被项目级覆盖 |
以为 permissions.allow 在项目级写一遍就会覆盖用户级 | 数组字段是"拼接 + 去重",不是覆盖。要"撤销"用户级的某项 allow,只能用项目级的 permissions.deny,因为 deny > allow |
把敏感个人路径(如 Read(/Volumes/work-secrets/**))写到项目级 | 项目级会进 git,团队人手一份。涉及个人本地路径、机器特有目录、临时实验放行,统统写到 settings.local.json |
以为 .claude/settings.local.json 必须自己手动 gitignore | Claude Code 在首次创建该文件时会自动追加到 .gitignore。但接手已有仓库时仍建议 grep 一下确认,避免历史 commit 残留 |
用 claude config 改完之后找不到改了哪一层 | 没有指定 scope 时默认改用户级。要指定具体层,需要直接编辑文件,或在 /config 菜单里看清"Source"列。/status 是排错神器 |
| 把 hooks 写到 local 级,期望队友也能用 | local 不进 git,队友拉不到。团队级 hooks 必须写项目级 .claude/settings.json,local 只用来个人临时挂钩 |
以为修改用户级 ~/.claude/settings.json 立刻全局生效 | Claude Code 进程启动时读取一次,热重载支持有限。改完关键字段(permissions / hooks / model)建议重启 Claude Code 或重新进入 session |
| 把企业 managed 当成"高级用户级" | managed 是强制天花板,普通 settings 改不动。如果 IT 在 managed 里写了 availableModels: ["claude-haiku-4-5"],你在用户级写 model: "claude-opus-4-7" 也启动不了 |
| 优势 | 劣势 |
|---|---|
| 三层结构刚好对应"个人 / 团队 / 例外",覆盖了真实协作场景 | 三层都可能有同名字段,新手容易迷失"现在到底哪层在生效",需要 /status 才能确认 |
| 数组拼接 + 去重的合并策略,让团队和个人各加各的 allow,互不删除对方 | 数组合并机制不直观——想"用项目级关掉用户级的某条 allow"做不到,只能 deny 反制 |
settings.local.json 自动 gitignore,避免最常见的"个人路径泄露到 git"事故 | 自动追加 .gitignore 只在首次创建时执行,接手历史仓库要自己检查 |
| 企业 managed 层支持 MDM/plist/注册表/远程下发,大企业可以做到强治理 | 企业 managed 一旦覆盖,普通用户完全无法看到自己被加了什么策略,排错全靠 /status |
JSON Schema 已上线(https://json.schemastore.org/claude-code-settings.json),编辑器有补全和校验 | settings 字段在快速演进,includeCoAuthoredBy 已被 attribution 替代,跟不上版本就会写错配置 |
| 跨项目偏好(用户级)和项目约定(项目级)独立演进,切项目时模型/语言不会被重置 | local 级不会自动同步到任何地方,换电脑要手动迁移 |
参考答案:
能。数组字段是合并不是覆盖,所以最终生效的 permissions.allow 是:
["Bash(npm run lint)", // 来自用户级
"Bash(npm run *)"] // 来自项目级
通配符 Bash(npm run *) 已经包含了 npm run test,所以放行。
需要警惕的反向场景:如果项目级写了 permissions.deny: ["Bash(curl *)"],而你在用户级写了 permissions.allow: ["Bash(curl *)"],deny 永远赢 allow,所以你这台机器上的 curl 仍然会被拦——这就是为什么团队约定优先放在 deny 而不是删 allow。
参考答案:
最佳实践:放在该仓库的 .claude/settings.local.json。理由有三层:
git push origin main 都会被放行,等于绕过了团队约定在所有项目上的兜底。.claude/settings.json 通常会写 permissions.deny: ["Bash(git push origin main)"],而 deny 优先级高于 allow,所以 local 级的 allow 反而会被项目级 deny 压住。正确做法是和团队沟通后,在项目级用更细粒度的规则,比如:
// 项目级 deny 改为更精确的形式,给"机器人 user 名"开一个口子
{
"permissions": {
"deny": ["Bash(git push origin main)"],
"allow": ["Bash(GIT_AUTHOR_NAME=release-bot git push origin main)"]
}
}
或者使用 hooks 在 PreToolUse 阶段动态判断,这部分见 /cli/cards/lifecycle-hooks。
结论一句话:local 级解决"我个人例外",但当例外和团队 deny 冲突时,必须回到项目级用更精确的 allow 规则,而不是去 local 里偷偷绕过。
优先展示同分类且标签更接近的内容,方便继续串联学习。
在终端 Agent 里挂 MCP server 的两种方式 —— claude mcp add 命令 vs settings.json 配置文件,含 stdio 与远程 server 的踩坑要点
理解 Claude Code 的生命周期 hook(PreToolUse / PostToolUse / Stop / Notification 等),何时该用、典型用例与避坑
理解 Claude Code 等 CLI Agent 的工具白名单 / 危险权限 / sandbox 边界,知道 bypassPermissions 何时该开