Skip to content

Commit dde884c

Browse files
authored
Merge pull request #85 from guyskk/feat/settings-env-override
feat: use --settings CLI param to override settings.json env conflicts
2 parents f542c01 + 881c01a commit dde884c

13 files changed

Lines changed: 504 additions & 848 deletions

README-CN.md

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,14 @@ ccc validate --all
8686

8787
运行 `ccc` 时,会读取你已有的 `settings.json` 并与 ccc.json 深度合并。优先级:**用户 `settings.json` > 提供商 > 基础 `settings`**。你手动编辑的配置、插件、hooks 都会被保留;提供商的环境变量通过命令行传递,不会写入 `settings.json`
8888

89-
#### 环境变量冲突(硬守卫)
89+
#### 环境变量处理
9090

91-
Claude Code 的 `settings.json` `env` 字段会**覆盖** ccc 启动 claude 时传入的环境变量。如果 `settings.json` 中存在会遮蔽 provider env 的 key,切换 provider 会静默失效(用错 base_url / token / model)
91+
Claude Code 的 `settings.json` `env` 字段会**覆盖** ccc 启动 claude 时传入的环境变量。为确保 provider env 优先生效,ccc 通过 `--settings` CLI 参数传入 provider env,该参数优先级高于 `settings.json`
9292

93-
**当检测到此类冲突时,ccc 会拒绝启动 claude,`ccc validate` 同样会被拒绝。** ccc 不会静默修改你的文件,而是会打印冲突的 key(不打印 value,避免泄露密钥)并告诉你如何修复。**ccc 不会修改你 `settings.json``env` 字段。**
94-
95-
满足以下任一条件的 key 视为冲突:
96-
-`ANTHROPIC_``CLAUDE_` 开头,****
97-
- 与 ccc.json 中 base / provider `env` 定义的任何 key 同名。
98-
99-
**修复方法**:从 `~/.claude/settings.json``env` 中删除这些 key,并把 provider 相关配置改到 `~/.claude/ccc.json``providers.<name>.env` 中。
93+
这意味着:
94+
- 你的 `settings.json` `env` 保持原样(ccc 不会修改它)
95+
- provider env 通过 `--settings` 自动覆盖冲突的 key
96+
- `settings.json` 中非冲突的 key 仍然正常工作
10097

10198
## Patch 命令:用 ccc 替代 `claude` 命令
10299

README.md

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,14 @@ Config file location, default: `~/.claude/ccc.json`
105105

106106
When you run `ccc`, your existing `settings.json` is read and deep-merged with ccc.json. Priority: **user `settings.json` > provider > base `settings`**. Your manual edits, plugins, and hooks are preserved; provider env is passed via command line and never written into `settings.json`.
107107

108-
#### Environment Variable Conflicts (Hard Guard)
108+
#### Environment Variable Handling
109109

110-
Claude Code's `settings.json` `env` field **overrides** environment variables passed by ccc when launching claude. If `settings.json` shadows provider env, switching silently fails (wrong base_url / token / model).
110+
Claude Code's `settings.json` `env` field **overrides** environment variables passed by ccc when launching claude. To ensure provider env takes precedence, ccc passes provider env via the `--settings` CLI parameter, which has higher priority than `settings.json`.
111111

112-
**ccc refuses to start claude — and refuses to run `ccc validate` — when it detects such conflicts.** It prints the offending keys (without values, to avoid leaking secrets) and never modifies your `settings.json` `env` field.
113-
114-
A key is considered conflicting if it:
115-
- starts with `ANTHROPIC_` or `CLAUDE_`, **or**
116-
- collides with any key defined in ccc.json's base / provider `env`.
117-
118-
**How to fix:** remove those keys from `~/.claude/settings.json`'s `env` and move provider-related configuration into `providers.<name>.env` in `~/.claude/ccc.json`.
112+
This means:
113+
- Your `settings.json` `env` is preserved as-is (ccc never modifies it)
114+
- Provider env automatically overrides conflicting keys via `--settings`
115+
- Non-conflicting keys in `settings.json` still work normally
119116

120117
```json
121118
{
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
# 讨论:env 冲突处理改进方案 — 能否用进程 env 覆盖 settings.json env
2+
3+
日期:2026-06-09
4+
话题:探索是否有更好的方式处理 settings.json env 与 ccc provider env 的冲突,是否能让 ccc 进程 env 覆盖 settings.json env。
5+
前置文档:docs/discuss-20260519-env-priority.md(已实测确认 settings.json env > 进程 env)
6+
7+
> 本文件 Append-only,只追加不覆盖。
8+
9+
---
10+
11+
## 第 1 轮:问题理解与现状分析
12+
13+
### 1.1 用户的核心问题
14+
15+
当前 ccc 处理 env 冲突的策略是"硬守卫":检测到 settings.json 的 `env` 中存在冲突 key(`ANTHROPIC_*`/`CLAUDE_*` 前缀或 managed key),直接报错中止,不启动 claude。
16+
17+
用户想知道:
18+
1. 能否在 ccc 中对冲突的环境变量,全部用进程环境变量进行覆盖?
19+
2. 即 settings.json `env` 的值 vs ccc claude 进程的 `env` 值冲突时,能否让 ccc 进程的 `env` 生效?
20+
21+
### 1.2 已知事实(来自 2026-05-19 实测)
22+
23+
**确定性结论**:Claude Code 中 settings.json 的 `env` 严格覆盖 claude 进程继承的 OS 环境变量。
24+
25+
- 实验 A:中性变量 `CCC_PRECEDENCE_TEST`,settings.json 的值覆盖进程 env
26+
- 实验 B:`ANTHROPIC_BASE_URL`,settings.json 的值覆盖进程 env(决定性实验)
27+
- 实验 C:settings.json 无 env 时,进程 env 兜底生效
28+
29+
**当前 ccc 的 env 传递链路**
30+
1. `os.Environ()` 继承 ccc 进程所有环境变量
31+
2. 过滤掉所有 `CLAUDE_*`/`ANTHROPIC_*` 前缀
32+
3. 追加 base + provider env
33+
4. `syscall.Exec` 替换为 claude 进程
34+
35+
但 Claude Code 启动后,自己读取 settings.json 的 `env` 字段,用其值覆盖进程 env → ccc 传入的 provider env 被 settings.json 中残留的同名 key 覆盖。
36+
37+
### 1.3 需要深入研究的方向
38+
39+
用户的问题本质上是:**有没有办法让进程 env 的优先级高于 settings.json env?**
40+
41+
可能的探索方向:
42+
1. **Claude Code 源码分析**:Claude Code 是如何读取 settings.json env 并应用到进程的?是否有命令行参数或环境变量可以改变这个行为?
43+
2. **Claude Code 版本变化**:之前的实测用的是 claude 2.1.144,当前最新版本是否有变化?
44+
3. **更高优先级的配置方式**:managed-settings.json 或命令行参数是否能覆盖 settings.json 的 env?
45+
4. **变通方案**:在启动 claude 前临时修改 settings.json(移除冲突 key),启动后再恢复?
46+
47+
---
48+
49+
## 第 2 轮:突破性发现 — `--settings` 参数的 env 覆盖能力
50+
51+
### 2.1 关键发现
52+
53+
Claude Code 提供 `--settings <file-or-json>` 命令行参数,描述为 "Path to a settings JSON file or a JSON string to load additional settings from"。
54+
55+
**settings 优先级(文档确认)**
56+
1. Enterprise (managed-settings.json) — 最高
57+
2. **Command line arguments** — 临时会话覆盖
58+
3. Local — 覆盖 project 和 user settings
59+
4. Project — 覆盖 user settings
60+
5. User (最低) — `~/.claude/settings.json` 属于此级别
61+
62+
`--settings` 属于 "Command line arguments" 级别,优先级**高于** User settings(`~/.claude/settings.json`)。
63+
64+
### 2.2 实验验证(claude 2.1.169)
65+
66+
**实验 1 — `--settings` env 是否生效**
67+
- settings.json:无 env
68+
- `--settings '{"env":{"CCC_SETTINGS_TEST":"VALUE_FROM_SETTINGS_PARAM"}}'`
69+
- 结果:`VALUE_FROM_SETTINGS_PARAM`
70+
71+
**实验 2 — `--settings` env 覆盖 settings.json env**
72+
- settings.json:`CCC_OVERRIDE_TEST=FROM_SETTINGS_JSON`
73+
- `--settings '{"env":{"CCC_OVERRIDE_TEST":"FROM_SETTINGS_PARAM"}}'`
74+
- 结果:`FROM_SETTINGS_PARAM` ✅ — `--settings` 覆盖了 settings.json
75+
76+
**实验 3 — 合并行为(决定性)**
77+
- settings.json:`CCC_TEST_A=A_FROM_SETTINGS_JSON`, `CCC_TEST_B=B_FROM_SETTINGS_JSON`
78+
- `--settings '{"env":{"CCC_TEST_B":"B_FROM_SETTINGS_PARAM","CCC_TEST_C":"C_FROM_SETTINGS_PARAM"}}'`
79+
- 结果:
80+
- `CCC_TEST_A` = `A_FROM_SETTINGS_JSON` ✅(settings.json 独有 key,保留)
81+
- `CCC_TEST_B` = `B_FROM_SETTINGS_PARAM` ✅(同名 key,`--settings` 覆盖)
82+
- `CCC_TEST_C` = `C_FROM_SETTINGS_PARAM` ✅(`--settings` 新增 key)
83+
84+
**确定性结论**`--settings``env``settings.json``env`**合并**关系,同名 key 由 `--settings` 覆盖,不同 key 各自保留。
85+
86+
### 2.3 对 ccc 的意义
87+
88+
这意味着 ccc 可以通过 `--settings` 参数传入 provider env,从而:
89+
1. **覆盖** settings.json 中的冲突 key(如 `ANTHROPIC_BASE_URL`),不需要修改 settings.json
90+
2. **保留** settings.json 中非冲突的用户自定义 env(如 `MY_CUSTOM_VAR`
91+
3. **避免报错中止**,用户体验更好
92+
93+
### 2.4 方案设计:用 `--settings` 参数传入 provider env
94+
95+
**核心思路**:在启动 claude 时,通过 `--settings` 参数传入包含 provider env 的 JSON,利用 Command line arguments 的高优先级覆盖 settings.json 中的冲突 env key。
96+
97+
**当前流程**(exec.go `runClaude()`):
98+
```
99+
1. 确定 provider
100+
2. checkSettingsEnvConflict() → 冲突则报错中止 ← 需要移除
101+
3. SwitchWithHook() → 合并配置、过滤 env、写入 settings.json ← env 逻辑需调整
102+
4. 构建 execArgs(claude 命令行参数)
103+
5. 构建进程 env:过滤 CLAUDE_*/ANTHROPIC_* + 追加 provider env
104+
6. syscall.Exec 启动 claude
105+
```
106+
107+
**新流程**
108+
```
109+
1. 确定 provider
110+
2. SwitchWithHook() → 合并配置、写入 settings.json(env 保留用户完整 env,不过滤冲突 key)
111+
3. 构建 execArgs:
112+
- 追加 --settings '{"env": {...provider env...}}'
113+
4. 构建进程 env:过滤 CLAUDE_*/ANTHROPIC_* + 追加 provider env(保持不变)
114+
5. syscall.Exec 启动 claude
115+
```
116+
117+
**关键变化**
118+
1. **移除 `checkSettingsEnvConflict()`**:不再需要硬守卫,因为 `--settings` 的 env 会覆盖 settings.json 的冲突 key
119+
2. **简化 `SwitchWithHook()` 的 env 处理**
120+
- settings.json 的 env 保留用户完整 env(不再用 `FilterUserEnvForSettings` 过滤冲突 key)
121+
- 子进程 env 构建不变(仍然只有 base + provider,不含 user env)
122+
3. **新增 `--settings` 参数构建**:将 provider env 序列化为 `{"env": {...}}` JSON,作为 `--settings` 参数追加到 execArgs
123+
124+
**预期效果**
125+
- settings.json 保留用户完整 env(包括 `ANTHROPIC_*`/`CLAUDE_*`)→ 用户手动配置不丢失
126+
- `--settings` 的 provider env 覆盖 settings.json 的冲突 key → provider 切换正确生效
127+
- settings.json 的非冲突 key(如 `MY_CUSTOM_VAR`)仍然保留 → 用户自定义 env 不受影响
128+
- 不需要报错中止 → 用户体验更好
129+
130+
### 2.5 需要验证的边界情况
131+
132+
1. **settings.json 没有 env**`--settings` 的 env 应该正常生效(实验 1 已验证)
133+
2. **settings.json 有 env 但不冲突**`--settings` 的 env 与 settings.json 的 env 合并,各自生效(实验 3 已验证)
134+
3. **settings.json 有 env 且冲突**`--settings` 的 env 覆盖 settings.json 的冲突 key(实验 2 已验证)
135+
4. **provider env 为空**:不需要传 `--settings` 参数
136+
5. **`--settings` 与其他 CLI 参数的兼容性**:需要验证 `--settings``-p``--model` 等参数是否兼容
137+
138+
### 2.6 代码修改范围(预估)
139+
140+
| 文件 | 修改内容 |
141+
|------|----------|
142+
| `internal/cli/exec.go` | 移除 `checkSettingsEnvConflict()`;在 `runClaude()` 中构建 `--settings` JSON 并追加到 execArgs |
143+
| `internal/provider/provider.go` | `SwitchWithHook()` 中 env 处理简化:不再过滤 settings.json 的 env,保留用户完整 env |
144+
| `internal/config/config.go` | `FilterUserEnvForSettings()` 函数可能不再需要(需确认是否有其他调用方) |
145+
| `internal/config/env_guard.go` | `DetectSettingsEnvConflicts()``FormatEnvConflictError()` 可能不再需要 |
146+
| 测试文件 | 更新相关测试 |
147+
148+
### 2.7 转义和环境变量引用验证(claude 2.1.169)
149+
150+
**实验 4 — 特殊字符转义**
151+
- `--settings '{"env":{"CCC_ESCAPE_TEST":"value with \"quotes\" and \\backslash and $HOME ref"}}'`
152+
- 结果:`value with "quotes" and \backslash and $HOME ref`
153+
- 双引号、反斜杠正确保留,`$HOME` 未展开(字面量)
154+
155+
**实验 5 — 环境变量引用**
156+
- 进程 env:`CCC_BASE_URL=http://example.com`
157+
- `--settings '{"env":{"CCC_EXPAND_TEST":"prefix-${CCC_BASE_URL}-suffix"}}'`
158+
- 结果:`prefix-${CCC_BASE_URL}-suffix` ✅(未展开,字面量)
159+
- 说明 `--settings` 的值不会被 claude 展开环境变量引用
160+
161+
**结论**
162+
1. Go 的 `json.Marshal` 生成的 JSON 可以正确传递特殊字符给 claude
163+
2. `--settings` 的值是字面量,ccc 在构建 JSON 时需要先用 `os.ExpandEnv()` 展开环境变量引用
164+
3. 这与当前 `envMapToPairs()` 的行为一致(它已经调用了 `os.ExpandEnv()`
165+
166+
### 2.8 用户决策
167+
168+
1. `--settings` 是稳定的正式 API ✅
169+
2. 转义问题需要处理好并实际验证 ✅(已验证)
170+
3. 不需要考虑 bare 模式 ✅
171+
4. 硬守卫不需要了 ✅(完全移除)
172+
173+
### 2.9 最终方案
174+
175+
**核心变更**:用 `--settings` 参数传入 provider env,替代硬守卫报错中止。
176+
177+
**代码修改**
178+
1. `internal/cli/exec.go`
179+
- 移除 `checkSettingsEnvConflict()` 函数及其调用
180+
-`runClaude()` 中构建 `--settings` JSON:将 provider env 序列化为 `{"env": {...}}`
181+
-`--settings` 和 JSON 追加到 `execArgs`
182+
2. `internal/provider/provider.go`
183+
- `SwitchWithHook()` 中移除 `FilterUserEnvForSettings` 调用
184+
- settings.json 的 env 保留用户完整 env(不再过滤冲突 key)
185+
- 子进程 env 构建不变(仍然只有 base + provider,不含 user env)
186+
3. `internal/config/config.go`
187+
- `FilterUserEnvForSettings()` 函数删除(无其他调用方)
188+
4. `internal/config/env_guard.go`
189+
- `DetectSettingsEnvConflicts()``FormatEnvConflictError()` 删除
190+
5. 测试文件:更新相关测试
191+
6. 文档:更新 settings-merge-strategy.md、README 等
192+
193+
**行为变化**
194+
- 之前:settings.json env 冲突 → 报错中止,用户必须手动清理
195+
- 之后:settings.json env 冲突 → `--settings` 自动覆盖冲突 key,正常启动
196+
- settings.json 保留用户完整 env → 用户手动配置不丢失
197+
198+
**状态:方案已完全对齐,无遗留疑问。**
199+
200+
---
201+
202+
## 第 3 轮:用户确认,进入计划模式
203+
204+
用户回复"好",确认方案。进入计划模式编写详细 Plan。
205+
206+
---
207+
208+
## 第 4 轮:实施完成
209+
210+
### 4.1 实施内容
211+
212+
按 TDD 流程完成所有代码修改:
213+
214+
1. **新增 `buildProviderSettingsJSON()` 函数**(exec.go):将 provider env 序列化为 `{"env": {...}}` JSON,环境变量引用预先展开
215+
2. **修改 `runClaude()`**(exec.go):移除 `checkSettingsEnvConflict` 调用,新增 `--settings` 参数构建
216+
3. **删除 `checkSettingsEnvConflict()` 函数**(exec.go)
217+
4. **修改 `SwitchWithHook()`**(provider.go):保留用户完整 env(不再过滤冲突 key),新增 `ProviderEnv` 字段
218+
5. **删除 `checkValidateEnvConflict()``validateTargetProviders()`**(cli.go)
219+
6. **删除 `env_guard.go``env_guard_test.go`**
220+
7. **删除 `FilterUserEnvForSettings()`**(config.go)
221+
8. **更新测试**:删除旧的冲突检测测试,新增 `buildProviderSettingsJSON` 测试,更新 provider 测试为新行为
222+
9. **更新文档**:README.md、README-CN.md、settings-merge-strategy.md
223+
224+
### 4.2 验证结果
225+
226+
- `./check.sh` 全量通过(lint + test + build)✅
227+
- 所有 133 个测试用例通过 ✅
228+
- 编译通过 ✅
229+
230+
### 4.3 行为变化
231+
232+
| 之前 | 之后 |
233+
|------|------|
234+
| settings.json env 冲突 → 报错中止 | settings.json env 冲突 → `--settings` 自动覆盖 |
235+
| settings.json 的冲突 key 被过滤删除 | settings.json 保留用户完整 env |
236+
| 用户必须手动清理 settings.json | 用户无需任何操作 |
237+
238+
---
239+
240+
## 第 5 轮:用户提问 — CLAUDE_*/ANTHROPIC_* 非冲突场景验证
241+
242+
### 5.1 用户问题
243+
244+
settings.json 有 `CLAUDE_*`/`ANTHROPIC_*` env,但 ccc provider 没有定义同名 key 时,这些 env 是否保留?
245+
246+
### 5.2 分析
247+
248+
`--settings` 的 env 与 settings.json 的 env 是**合并**关系:
249+
- 同名 key → `--settings` 覆盖
250+
- 不同 key → 各自保留
251+
252+
如果 settings.json 有 `CLAUDE_CODE_MAX_OUTPUT_TOKENS=1000`,而 provider env 没有这个 key,那么 `--settings` JSON 中不包含它,settings.json 的值保留生效。
253+
254+
**应该是正确行为** — 用户设置的非冲突 env 应该保留。
255+
256+
### 5.3 完整验证(claude 2.1.169,真实进程)
257+
258+
**场景 1 — 冲突覆盖**
259+
- settings.json: `ANTHROPIC_BASE_URL=http://settings-json:19991`
260+
- `--settings`: `ANTHROPIC_BASE_URL=http://provider:19992`
261+
- 结果:连接 `provider:19992` ✅(`--settings` 覆盖)
262+
263+
**场景 2b — 非冲突保留**
264+
- settings.json: `CCC_TEST_CLAUDE_VAR=HELLO_FROM_SETTINGS`
265+
- `--settings`: `CCC_TEST_PROVIDER_VAR=HELLO_FROM_PROVIDER`
266+
- 结果:两个 env 都保留 ✅
267+
268+
**场景 3 — ANTHROPIC_MODEL 非冲突保留**
269+
- settings.json: `ANTHROPIC_MODEL=my-custom-model-from-settings`
270+
- `--settings`: 无此 key
271+
- 结果:claude 使用 `my-custom-model-from-settings`(400 错误 = 模型名无效)✅
272+
273+
**场景 4b — 冲突+非冲突混合**
274+
- settings.json: `CCC_CONFLICT_KEY=from-settings`, `CCC_KEPT_KEY=kept-from-settings`
275+
- `--settings`: `CCC_CONFLICT_KEY=from-provider`
276+
- 结果:`CCC_CONFLICT_KEY=from-provider`(覆盖),`CCC_KEPT_KEY=kept-from-settings`(保留)✅
277+
278+
**场景 5 — 多个 CLAUDE_*/ANTHROPIC_* 非冲突保留**
279+
- settings.json: `CLAUDE_CODE_MAX_OUTPUT_TOKENS=2000`, `CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR=1`, `ANTHROPIC_SMALL_FAST_MODEL=claude-haiku-3`
280+
- `--settings`: 无这些 key
281+
- 结果:三个 env 全部保留 ✅
282+
283+
**场景 6 — 进程 env 保留**
284+
- 进程 env: `ANTHROPIC_PROCESS_TEST=anthropic-from-process`
285+
- settings.json: 无 env
286+
- 结果:进程 env 保留 ✅
287+
288+
**场景 7 — 完整链路(模拟 ccc)**
289+
- settings.json: `ANTHROPIC_BASE_URL=http://old-settings:19991`, `MY_VAR=keep-me`, `API_TIMEOUT=99999`
290+
- `--settings`: `API_TIMEOUT=30000`, `ANTHROPIC_BASE_URL=http://ccc-provider:19992`, `ANTHROPIC_AUTH_TOKEN=sk-test`
291+
- 结果:`ANTHROPIC_BASE_URL` 覆盖为 `ccc-provider:19992``API_TIMEOUT` 覆盖为 `30000``MY_VAR` 保留 ✅
292+
293+
### 5.4 结论
294+
295+
所有 7 个场景验证通过。`--settings` 的合并机制正确处理了:
296+
1. 冲突 key 覆盖 ✅
297+
2. 非冲突 key 保留(包括 CLAUDE_*/ANTHROPIC_* 前缀)✅
298+
3. 进程 env 保留 ✅
299+
4. 混合场景正确处理 ✅
300+
301+
**用户在 settings.json 中手动配置的 CLAUDE_*/ANTHROPIC_* env(如 `CLAUDE_CODE_MAX_OUTPUT_TOKENS`),只要 provider 没有定义同名 key,就会保留生效。这是正确行为。**
302+
303+
---
304+

0 commit comments

Comments
 (0)